@mastra/factory 0.24.0-alpha.11 → 0.24.0-alpha.12

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 (136) hide show
  1. package/README.md +4 -4
  2. package/dist/environment/build-runner.js +2 -1
  3. package/dist/environment/build-runner.js.map +1 -1
  4. package/dist/environment/build-workflow.js +1 -1
  5. package/dist/environment/build-workflow.js.map +1 -1
  6. package/dist/environment/build.js +16 -14
  7. package/dist/environment/build.js.map +1 -1
  8. package/dist/factory.js.map +1 -1
  9. package/dist/integrations/github/app-identity.js.map +1 -1
  10. package/dist/integrations/github/integration.js +1 -1
  11. package/dist/integrations/github/integration.js.map +1 -1
  12. package/dist/integrations/github/reconcile-worker.js +1 -1
  13. package/dist/integrations/github/reconcile-worker.js.map +1 -1
  14. package/dist/integrations/github/routes.js +1 -1
  15. package/dist/integrations/github/routes.js.map +1 -1
  16. package/dist/integrations/github/rules.js +3 -2
  17. package/dist/integrations/github/rules.js.map +1 -1
  18. package/dist/integrations/github/sandbox.js +2 -2
  19. package/dist/integrations/github/sandbox.js.map +1 -1
  20. package/dist/integrations/gitlab/api.js.map +1 -1
  21. package/dist/integrations/gitlab/integration.js.map +1 -1
  22. package/dist/integrations/gitlab/reconciliation-config.js +2 -1
  23. package/dist/integrations/gitlab/reconciliation-config.js.map +1 -1
  24. package/dist/integrations/gitlab/rules.js.map +1 -1
  25. package/dist/integrations/incidentio/api.js.map +1 -1
  26. package/dist/integrations/incidentio/integration.js.map +1 -1
  27. package/dist/integrations/incidentio/rules.js.map +1 -1
  28. package/dist/integrations/issue-reconcile-worker.js +1 -1
  29. package/dist/integrations/issue-reconcile-worker.js.map +1 -1
  30. package/dist/integrations/issue-reconciler.js +1 -1
  31. package/dist/integrations/issue-reconciler.js.map +1 -1
  32. package/dist/integrations/jira/api.js.map +1 -1
  33. package/dist/integrations/jira/integration.js.map +1 -1
  34. package/dist/integrations/jira/rules.js.map +1 -1
  35. package/dist/integrations/linear/integration.js.map +1 -1
  36. package/dist/integrations/linear/rules.js.map +1 -1
  37. package/dist/integrations/platform/api-client.js.map +1 -1
  38. package/dist/integrations/platform/github/event-worker.js +1 -1
  39. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  40. package/dist/integrations/platform/github/integration.js +2 -2
  41. package/dist/integrations/platform/github/integration.js.map +1 -1
  42. package/dist/integrations/platform/gitlab/event-worker.js.map +1 -1
  43. package/dist/integrations/platform/gitlab/integration.js.map +1 -1
  44. package/dist/integrations/platform/incidentio/integration.js +2 -1
  45. package/dist/integrations/platform/incidentio/integration.js.map +1 -1
  46. package/dist/integrations/platform/jira/integration.js.map +1 -1
  47. package/dist/integrations/platform/linear/event-worker.js +1 -1
  48. package/dist/integrations/platform/linear/event-worker.js.map +1 -1
  49. package/dist/integrations/platform/linear/integration.js.map +1 -1
  50. package/dist/integrations/slack/connect-route.js +1 -1
  51. package/dist/integrations/slack/connect-route.js.map +1 -1
  52. package/dist/integrations/slack/feed-publisher.js +1 -1
  53. package/dist/integrations/slack/feed-publisher.js.map +1 -1
  54. package/dist/integrations/slack/integration.js +1 -1
  55. package/dist/integrations/slack/integration.js.map +1 -1
  56. package/dist/integrations/slack/slack.js +2 -1
  57. package/dist/integrations/slack/slack.js.map +1 -1
  58. package/dist/integrations/workos/integration.js.map +1 -1
  59. package/dist/packages/_internals/workspace/dist/index.js +18 -1
  60. package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
  61. package/dist/routes/attention-activity.js.map +1 -1
  62. package/dist/routes/attention-parked.js.map +1 -1
  63. package/dist/routes/attention-providers.js.map +1 -1
  64. package/dist/routes/automation-runs.js.map +1 -1
  65. package/dist/routes/contracts.js +1 -1
  66. package/dist/routes/contracts.js.map +1 -1
  67. package/dist/routes/custom-provider-source.js.map +1 -1
  68. package/dist/routes/fs.d.ts.map +1 -1
  69. package/dist/routes/fs.js +4 -3
  70. package/dist/routes/fs.js.map +1 -1
  71. package/dist/routes/intake.js +2 -1
  72. package/dist/routes/intake.js.map +1 -1
  73. package/dist/routes/knowledge.js +1 -1
  74. package/dist/routes/knowledge.js.map +1 -1
  75. package/dist/routes/oauth.js +3 -2
  76. package/dist/routes/oauth.js.map +1 -1
  77. package/dist/routes/projects.js +32 -30
  78. package/dist/routes/projects.js.map +1 -1
  79. package/dist/routes/skills.js.map +1 -1
  80. package/dist/routes/source-control-sessions.js +1 -1
  81. package/dist/routes/source-control-sessions.js.map +1 -1
  82. package/dist/routes/surface.js +8 -2
  83. package/dist/routes/surface.js.map +1 -1
  84. package/dist/routes/tenant-credentials.js.map +1 -1
  85. package/dist/routes/work-items.js.map +1 -1
  86. package/dist/rules/binding-context.js +4 -2
  87. package/dist/rules/binding-context.js.map +1 -1
  88. package/dist/rules/dispatcher.js +4 -4
  89. package/dist/rules/dispatcher.js.map +1 -1
  90. package/dist/rules/start-coordinator.js.map +1 -1
  91. package/dist/rules/tools.js +2 -2
  92. package/dist/rules/tools.js.map +1 -1
  93. package/dist/rules/transition-service.js.map +1 -1
  94. package/dist/sandbox/callback-factory-sandbox.js.map +1 -1
  95. package/dist/sandbox/local-factory-sandbox.js.map +1 -1
  96. package/dist/sandbox/session-retirement.js.map +1 -1
  97. package/dist/session/live-sessions.js.map +1 -1
  98. package/dist/session/memory-settings-hydration.js +2 -1
  99. package/dist/session/memory-settings-hydration.js.map +1 -1
  100. package/dist/spa-static.js.map +1 -1
  101. package/dist/state-signing.js +1 -1
  102. package/dist/state-signing.js.map +1 -1
  103. package/dist/storage/domains/audit/base.js.map +1 -1
  104. package/dist/storage/domains/audit/domain.js.map +1 -1
  105. package/dist/storage/domains/channel-identity/base.js.map +1 -1
  106. package/dist/storage/domains/comments/base.js.map +1 -1
  107. package/dist/storage/domains/comments/domain.js.map +1 -1
  108. package/dist/storage/domains/comments/feed-context.js.map +1 -1
  109. package/dist/storage/domains/credentials/base.js.map +1 -1
  110. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  111. package/dist/storage/domains/intake/base.js.map +1 -1
  112. package/dist/storage/domains/integrations/base.js.map +1 -1
  113. package/dist/storage/domains/memory-settings/base.js.map +1 -1
  114. package/dist/storage/domains/model-defaults/base.js.map +1 -1
  115. package/dist/storage/domains/projects/base.js.map +1 -1
  116. package/dist/storage/domains/queue-health/base.js.map +1 -1
  117. package/dist/storage/domains/source-control/base.js.map +1 -1
  118. package/dist/storage/domains/work-items/base.js +2 -1
  119. package/dist/storage/domains/work-items/base.js.map +1 -1
  120. package/dist/storage/domains/work-items/board-version.js.map +1 -1
  121. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  122. package/dist/tools/messaging-tools.d.ts.map +1 -1
  123. package/dist/tools/messaging-tools.js +201 -73
  124. package/dist/tools/messaging-tools.js.map +1 -1
  125. package/dist/tools/run-watch.d.ts +2 -0
  126. package/dist/tools/run-watch.d.ts.map +1 -1
  127. package/dist/tools/run-watch.js +42 -2
  128. package/dist/tools/run-watch.js.map +1 -1
  129. package/dist/tools/session-tools.d.ts.map +1 -1
  130. package/dist/tools/session-tools.js +24 -9
  131. package/dist/tools/session-tools.js.map +1 -1
  132. package/dist/tools/sessions-state-processor.js +2 -2
  133. package/dist/tools/sessions-state-processor.js.map +1 -1
  134. package/dist/workspace.js +15 -12
  135. package/dist/workspace.js.map +1 -1
  136. package/package.json +7 -7
@@ -1 +1 @@
1
- {"version":3,"file":"messaging-tools.js","names":[],"sources":["../../src/tools/messaging-tools.ts"],"sourcesContent":["/**\n * Messages between Factory sessions. A user session can message any session in\n * its Factory it can see; the receiver answers that exact message. Both\n * directions are `factory-session` notifications on the receiver's stored thread.\n */\n\nimport type { NotificationPriority, NotificationRecord, NotificationsStorage } from '@mastra/core/notifications';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport type { IntegrationTools } from '../integrations/base.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { SourceControlSession } from '../storage/domains/source-control/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport {\n FACTORY_SESSION_NOTIFICATION_SOURCE,\n deliverFactorySessionNotification,\n resolveSessionThread,\n} from './deliver.js';\nimport type { FactoryDeliveryController, FactoryDeliveryResult } from './deliver.js';\nimport { workItemNumber } from './read-tools.js';\nimport { reportNotification, workItemReportAttributes, workItemReportRecipients } from './reports.js';\nimport { requirePerson } from './scope.js';\nimport type { FactoryToolScope } from './scope.js';\nimport { listSideSessions, sessionModel } from './side-sessions.js';\n\n/** Messages plus replies, per sender session, per target thread, per window. */\nexport const SIGNAL_RATE_LIMIT = 10;\nexport const SIGNAL_RATE_WINDOW_MS = 10 * 60 * 1000;\n/** How many recent `factory-session` notifications on a thread the rate and duplicate checks read. */\nconst RECENT_SCAN = 100;\nconst RATE_LIMITED = 'Too many messages to that session in the last 10 minutes; wait before sending more.';\n\nexport interface FactoryMessaging {\n controller: FactoryDeliveryController;\n notifications: Pick<NotificationsStorage, 'listNotifications' | 'getNotification'>;\n}\n\nexport interface FactoryMessagingToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'listByOrigin' | 'setOriginMuted'>;\n workItems: Pick<\n WorkItemsStorage,\n | 'findActiveRunBindingForSession'\n | 'getForProject'\n | 'hasRunBindingForSession'\n | 'listSessionSubscriptions'\n | 'listRunBindings'\n | 'listWorkItemSubscribers'\n >;\n auditRecorder: AuditRecorder;\n /** The live controller and notifications store; resolved per call because they exist only once the mount is up. */\n messaging: () => Promise<FactoryMessaging | null>;\n}\n\nconst message = z.string().trim().min(1).max(4000);\n\nfunction senderAttributes(scope: FactoryToolScope) {\n return {\n 'from-session': scope.sessionId,\n 'from-thread': scope.threadId,\n 'from-user': scope.userId,\n };\n}\n\nfunction unavailable(result: Extract<FactoryDeliveryResult, { delivered: 'skipped' }>) {\n return new Error(`The session is not available (${result.reason}).`);\n}\n\nexport function createFactoryMessagingTools(\n scope: FactoryToolScope,\n deps: FactoryMessagingToolDependencies,\n): IntegrationTools {\n const requireMessaging = async () => {\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Session messaging is not available on this server.');\n return messaging;\n };\n // Only a user-session turn the person started is the person's act. Workers\n // (even when someone types into one) and turns nobody signed in started\n // (wakes, webhooks) act as the thread's agent.\n const human = scope.kind === 'user' && scope.interactive;\n const audit = (input: { targetSessionId: string; priority: NotificationPriority; kind: string; delivery: string }) =>\n deps.auditRecorder.record({\n orgId: scope.orgId,\n actorId: human ? scope.userId : `agent:${scope.threadId}`,\n actorType: human ? 'human' : 'agent',\n action: 'factory.agent.signaled',\n factoryProjectId: scope.factoryProjectId,\n targets: [{ type: 'session', id: input.targetSessionId }],\n metadata: {\n targetSessionId: input.targetSessionId,\n priority: input.priority,\n kind: input.kind,\n delivery: input.delivery,\n },\n });\n /** Why this session can't reach `row`, or null when it can: the rule for message targets and reply targets alike. */\n const unreachable = (row: SourceControlSession | null): 'not-found' | 'private' | null => {\n if (!row || row.orgId !== scope.orgId || row.factoryProjectId !== scope.factoryProjectId) return 'not-found';\n if (row.visibility === 'private' && row.userId !== scope.userId) return 'private';\n return null;\n };\n /** Messages, replies and reports (not progress) this session put on a thread inside the rate window. */\n const overRateLimit = (records: NotificationRecord[]) => {\n const since = Date.now() - SIGNAL_RATE_WINDOW_MS;\n return (\n records.filter(\n record =>\n // Everything this session sends counts except coalescing progress updates.\n record.kind !== 'progress' &&\n record.kind !== 'stage' &&\n record.attributes?.['from-session'] === scope.sessionId &&\n record.createdAt.getTime() >= since,\n ).length >= SIGNAL_RATE_LIMIT\n );\n };\n const recent = (notifications: FactoryMessaging['notifications'], threadId: string) =>\n notifications.listNotifications({ threadId, source: FACTORY_SESSION_NOTIFICATION_SOURCE, limit: RECENT_SCAN });\n\n /** On turns nobody signed in started, only sessions working on items this session follows. */\n const followedWorkers = async () => {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n const follows = await deps.workItems.listSessionSubscriptions({ ...project, sessionId: scope.sessionId });\n const bindings = await Promise.all(\n follows.map(follow => deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, follow.workItemId)),\n );\n const sideSessions = await deps.sessions.listByOrigin({ orgId: scope.orgId, originSessionId: scope.sessionId });\n return new Set([\n ...bindings\n .flat()\n .filter(binding => binding.status === 'active')\n .map(binding => binding.sessionId),\n ...sideSessions.map(session => session.sessionId),\n ]);\n };\n\n const workItemAttribute = async (sessionId: string, orgId: string, factoryProjectId: string) => {\n const binding = await deps.workItems.findActiveRunBindingForSession({ orgId, factoryProjectId, sessionId });\n if (!binding) return {};\n const item = await deps.workItems.getForProject(orgId, factoryProjectId, binding.workItemId);\n if (!item) return {};\n const number = workItemNumber(item);\n return { 'work-item': number === null ? item.id : `#${number}` };\n };\n\n const signal = createTool({\n id: 'factory_signal_session',\n description: [\n 'Send a message to another session in this Factory: a work item role session, or a teammate session you can see.',\n 'Replies come back to this session as `factory-session` notifications with kind `reply` and an `in-reply-to` attribute naming your message.',\n 'Priority `medium` (default) wakes an idle session and waits for a busy one to finish its step; use `urgent` only to make it stop and change course.',\n '`delivered` is `woken` when the session was idle and is now running, `mid-run` when it was already busy and will read the message when its run next checks in.',\n ...(scope.interactive\n ? []\n : ['On this turn you can only message your side sessions and sessions working on items this session follows.']),\n ].join(' '),\n inputSchema: z\n .object({\n sessionId: z.string().trim().min(1).max(200).describe('The session to message.'),\n message: message.describe('What to say. It arrives as-is, so make it self-contained.'),\n priority: z.enum(['low', 'medium', 'high', 'urgent']).default('medium'),\n })\n .strict(),\n execute: async ({ sessionId, message: text, priority }) => {\n if (sessionId === scope.sessionId) throw new Error(\"You can't message your own session.\");\n const row = await deps.sessions.getBySessionId(sessionId);\n const blocked = unreachable(row);\n if (blocked === 'not-found' || !row) throw new Error(`No session ${sessionId} was found in this Factory.`);\n if (blocked === 'private') throw new Error(`Session ${sessionId} is private to its owner.`);\n if (!scope.interactive && !(await followedWorkers()).has(sessionId)) {\n throw new Error(\n `On this turn you can only message this session's side sessions and sessions working on items it follows; ${sessionId} isn't one.`,\n );\n }\n\n const messaging = await requireMessaging();\n const threadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId,\n });\n if (!threadId) throw new Error('The session is not available (thread-not-found).');\n\n if (overRateLimit(await recent(messaging.notifications, threadId))) {\n throw new Error(RATE_LIMITED);\n }\n\n const result = await deliverFactorySessionNotification({\n controller: messaging.controller,\n sessions: deps.sessions,\n runs: deps.workItems,\n target: { sessionId, threadId },\n notification: {\n kind: 'message',\n priority,\n summary: text,\n payload: { message: text },\n sourceId: scope.sessionId,\n attributes: {\n ...(await workItemAttribute(sessionId, row.orgId, scope.factoryProjectId)),\n ...senderAttributes(scope),\n },\n },\n });\n if (result.delivered === 'skipped') throw unavailable(result);\n await audit({ targetSessionId: sessionId, priority, kind: 'message', delivery: result.delivered });\n return { notificationId: result.notificationId, delivered: result.delivered };\n },\n });\n\n const reply = createTool({\n id: 'factory_message_origin',\n description: [\n 'Answer a message another Factory session sent you, or report on your work.',\n 'With `inReplyTo` (the id of a `factory-session` notification of kind `message`) the answer goes back to the session and thread that sent it; each message can be answered once.',\n 'Without it, the report goes to whoever follows your work: the session that started you, or everyone following your work item.',\n \"Set `status`: `progress` for an update, `blocked` when you need an answer to go on, `done` when finished, `failed` when you can't finish. Send one `done` or `failed` when you finish.\",\n ].join(' '),\n inputSchema: z\n .object({\n message: message.describe('Your answer.'),\n inReplyTo: z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe('Id of the message notification you are answering.'),\n status: z.enum(['progress', 'blocked', 'done', 'failed']).optional(),\n })\n .strict(),\n execute: async ({ message: text, inReplyTo, status }) => {\n if (!inReplyTo) return report(text, status);\n const messaging = await requireMessaging();\n // Only a message that reached this session's own thread can be answered,\n // and the answer goes where that message came from: never to an address\n // the model supplies.\n const original: NotificationRecord | null = await messaging.notifications.getNotification({\n threadId: scope.threadId,\n id: inReplyTo,\n });\n if (!original || original.source !== FACTORY_SESSION_NOTIFICATION_SOURCE) {\n throw new Error(`No message ${inReplyTo} was sent to this session.`);\n }\n if (original.kind !== 'message') {\n throw new Error(`Only messages can be answered; ${inReplyTo} is a ${original.kind}.`);\n }\n const fromSession = original.attributes?.['from-session'];\n const fromThread = original.attributes?.['from-thread'];\n if (typeof fromSession !== 'string' || typeof fromThread !== 'string' || original.sourceId !== fromSession) {\n throw new Error(`Message ${inReplyTo} doesn't say which session sent it.`);\n }\n // Notification records can be written outside these tools, so the\n // sender they name gets the same checks a message to it would.\n const blocked = unreachable(await deps.sessions.getBySessionId(fromSession));\n if (blocked === 'not-found') throw new Error(`Message ${inReplyTo} didn't come from a session in this Factory.`);\n if (blocked === 'private') {\n throw new Error(`Message ${inReplyTo} came from a private session this session can't answer.`);\n }\n const onSenderThread = await recent(messaging.notifications, fromThread);\n // Replies count toward the same cap as messages, so records written to\n // this session can't turn it into an unlimited high-priority sender.\n const answered = onSenderThread.some(\n record =>\n record.kind === 'reply' &&\n record.attributes?.['in-reply-to'] === inReplyTo &&\n record.attributes?.['from-session'] === scope.sessionId,\n );\n if (answered) throw new Error(`You already answered ${inReplyTo}.`);\n if (overRateLimit(onSenderThread)) throw new Error(RATE_LIMITED);\n\n const result = await deliverFactorySessionNotification({\n controller: messaging.controller,\n sessions: deps.sessions,\n runs: deps.workItems,\n target: { sessionId: fromSession, threadId: fromThread },\n notification: {\n kind: 'reply',\n priority: 'high',\n summary: text,\n payload: { message: text },\n sourceId: scope.sessionId,\n attributes: {\n ...senderAttributes(scope),\n 'in-reply-to': inReplyTo,\n ...(status ? { status } : {}),\n },\n },\n });\n if (result.delivered === 'skipped') throw unavailable(result);\n await audit({ targetSessionId: fromSession, priority: 'high', kind: 'reply', delivery: result.delivered });\n return { notificationId: result.notificationId, delivered: result.delivered };\n },\n });\n\n /** A report with no message to answer: to the origin, else to the item's followers. */\n async function report(text: string, status: 'progress' | 'blocked' | 'done' | 'failed' | undefined) {\n const sender = { sessionId: scope.sessionId, threadId: scope.threadId, userId: scope.userId };\n let recipients: Array<{ sessionId: string; threadId: string }>;\n let attributes: Record<string, string> = {};\n if (scope.origin) {\n // The origin can mute this side session; the session keeps working, its reports just stop.\n // Blocked reports still go through, since the session cannot go on until the origin answers.\n const row = await deps.sessions.getBySessionId(scope.sessionId);\n if (row?.originMutedAt && status !== 'blocked') {\n return { delivered: 0, recipients: [], muted: true, note: 'Your origin muted your reports; keep working.' };\n }\n recipients = [scope.origin];\n } else {\n const binding = await deps.workItems.findActiveRunBindingForSession({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n });\n if (!binding) throw new Error('Nothing started this session; pass inReplyTo to answer a message.');\n recipients = await workItemReportRecipients(deps.workItems, {\n orgId: scope.orgId,\n workItemId: binding.workItemId,\n senderSessionId: scope.sessionId,\n });\n ({ attributes } = await workItemReportAttributes(deps.workItems, {\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n workItemId: binding.workItemId,\n role: binding.role,\n }));\n }\n if (recipients.length === 0) return { delivered: 0, recipients: [] };\n\n const messaging = await requireMessaging();\n const notification = reportNotification({ sender, status, message: text, attributes });\n const results: Array<{ sessionId: string; delivered: FactoryDeliveryResult['delivered']; reason?: string }> = [];\n for (const recipient of recipients) {\n // Progress replaces itself while unread; anything else can wake a follower, so it counts.\n if (status !== 'progress' && overRateLimit(await recent(messaging.notifications, recipient.threadId))) {\n results.push({ sessionId: recipient.sessionId, delivered: 'skipped', reason: 'rate-limited' });\n continue;\n }\n const result = await deliverFactorySessionNotification({\n controller: messaging.controller,\n sessions: deps.sessions,\n runs: deps.workItems,\n target: recipient,\n notification,\n });\n results.push({ sessionId: recipient.sessionId, delivered: result.delivered });\n if (result.delivered === 'skipped') continue;\n await audit({\n targetSessionId: recipient.sessionId,\n priority: notification.priority ?? 'high',\n kind: notification.kind,\n delivery: result.delivered,\n });\n }\n const delivered = results.filter(result => result.delivered !== 'skipped').length;\n if (delivered === 0 && results.every(result => result.reason === 'rate-limited')) throw new Error(RATE_LIMITED);\n return { delivered, recipients: results };\n }\n\n const listSide = createTool({\n id: 'factory_list_side_sessions',\n description: [\n 'List the side sessions this session started with factory_start_session: state (running, parked waiting on an answer, idle), last report, whether you muted it, its model, and the work item it works on.',\n 'Use the session ids with factory_signal_session and factory_switch_session_model. On a turn a person started, also with factory_answer_approval, factory_unsubscribe_session and factory_subscribe_session.',\n ].join(' '),\n inputSchema: z\n .object({\n state: z.enum(['running', 'parked', 'idle']).optional().describe('Only sessions in this state.'),\n muted: z.boolean().optional().describe('true for only muted sessions, false for only unmuted ones.'),\n })\n .strict(),\n execute: async ({ state, muted }) => {\n const messaging = await deps.messaging();\n const sessions = await listSideSessions(\n {\n sessions: deps.sessions,\n workItems: deps.workItems,\n notifications: messaging?.notifications ?? null,\n isRunning: async sessionId => (await messaging?.controller.getSession(sessionId))?.stream.isActive() ?? false,\n model: messaging\n ? sessionId => sessionModel(messaging.controller, deps.sessions, deps.workItems, sessionId)\n : undefined,\n },\n scope,\n );\n return {\n sessions: sessions.filter(\n row => (!state || row.state === state) && (muted === undefined || (row.mutedAt !== null) === muted),\n ),\n };\n },\n });\n\n /** A side session this session started, or an error naming why it isn't one. */\n const ownSideSession = async (sessionId: string) => {\n const row = await deps.sessions.getBySessionId(sessionId);\n if (\n !row ||\n row.orgId !== scope.orgId ||\n row.factoryProjectId !== scope.factoryProjectId ||\n row.originSessionId !== scope.sessionId\n ) {\n throw new Error(`Session ${sessionId} is not a side session you started; see factory_list_side_sessions.`);\n }\n return row;\n };\n const sideSessionId = z\n .string()\n .trim()\n .min(1)\n .max(200)\n .describe('The side session, from factory_list_side_sessions.');\n\n const unsubscribe = createTool({\n id: 'factory_unsubscribe_session',\n description:\n \"Mute a side session you started: its reports and run-end notices stop reaching you, but it keeps working. Approval requests and blocked reports still arrive, since the session can't go on without an answer. Undo with factory_subscribe_session. Only on a turn a person started.\",\n inputSchema: z.object({ sessionId: sideSessionId }).strict(),\n execute: async ({ sessionId }) => {\n requirePerson(scope, 'mute a side session');\n const row = await ownSideSession(sessionId);\n if (row.originMutedAt) return { sessionId, muted: true, mutedAt: row.originMutedAt.toISOString() };\n await deps.sessions.setOriginMuted({ sessionId, muted: true });\n const updated = await deps.sessions.getBySessionId(sessionId);\n return { sessionId, muted: true, mutedAt: updated?.originMutedAt?.toISOString() ?? null };\n },\n });\n\n const subscribe = createTool({\n id: 'factory_subscribe_session',\n description:\n 'Unmute a side session you muted with factory_unsubscribe_session, so its reports reach you again. Reports it sent while muted are not replayed. Only on a turn a person started.',\n inputSchema: z.object({ sessionId: sideSessionId }).strict(),\n execute: async ({ sessionId }) => {\n requirePerson(scope, 'unmute a side session');\n const row = await ownSideSession(sessionId);\n if (row.originMutedAt) await deps.sessions.setOriginMuted({ sessionId, muted: false });\n return { sessionId, muted: false };\n },\n });\n\n if (scope.kind === 'worker') return { factory_message_origin: reply };\n return {\n factory_signal_session: signal,\n factory_message_origin: reply,\n factory_list_side_sessions: listSide,\n factory_unsubscribe_session: unsubscribe,\n factory_subscribe_session: subscribe,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"messaging-tools.js","names":[],"sources":["../../src/tools/messaging-tools.ts"],"sourcesContent":["/**\n * Messages between Factory sessions. A user session can message any session in\n * its Factory it can see; the receiver answers that exact message. Both\n * directions are `factory-session` notifications on the receiver's stored thread.\n */\n\nimport type {\n NotificationPriority,\n NotificationRecord,\n NotificationsStorage,\n SendNotificationSignalInput,\n} from '@mastra/core/notifications';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport type { IntegrationTools } from '../integrations/base.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { SourceControlSession } from '../storage/domains/source-control/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport {\n FACTORY_SESSION_NOTIFICATION_SOURCE,\n deliverFactorySessionNotification,\n resolveSessionThread,\n} from './deliver.js';\nimport type { FactoryDeliveryController, FactoryDeliveryResult } from './deliver.js';\nimport { workItemNumber } from './read-tools.js';\nimport { reportNotification, workItemReportAttributes, workItemReportRecipients } from './reports.js';\nimport { requirePerson } from './scope.js';\nimport type { FactoryToolScope } from './scope.js';\nimport { listSideSessions, sessionModel } from './side-sessions.js';\n\n/** Messages plus replies, per sender session, per target thread, per window. */\nexport const SIGNAL_RATE_LIMIT = 10;\nexport const SIGNAL_RATE_WINDOW_MS = 10 * 60 * 1000;\n/** How many recent `factory-session` notifications on a thread the rate and duplicate checks read. */\nconst RECENT_SCAN = 100;\n/** How often a waiting queue is reconsidered. Delivery still waits for a free slot in the window. */\nconst QUEUE_FLUSH_MS = 1000;\n\n/** A message accepted past the delivery cap, held until the window has a free slot. */\ninterface PendingMessage {\n notification: Omit<SendNotificationSignalInput, 'source'>;\n /** When the sender handed it over. Oldest goes first. */\n acceptedAt: number;\n actor: { actorId: string; actorType: 'human' | 'agent' };\n}\n\ninterface QueuedDelivery extends PendingMessage {\n /** The moment a slot in the window is expected to free. Set once, when it joins the queue. */\n deliverAt: number;\n target: { sessionId: string; threadId: string };\n}\n\n/**\n * Waiting messages, per sender session and target thread, oldest first.\n * Process-local: a restart drops what was still waiting, the same as a message\n * that had not been handed to the tool yet. Delivered messages live in the\n * notification store and are unaffected.\n */\nconst pendingQueues = new Map<string, QueuedDelivery[]>();\nconst flushTimers = new Map<string, ReturnType<typeof setTimeout>>();\n\nconst queueOwners = new WeakMap<object, number>();\nlet nextQueueOwnerId = 0;\n\nconst queueKey = (owner: object, senderSessionId: string, threadId: string): string => {\n let id = queueOwners.get(owner);\n if (id === undefined) {\n nextQueueOwnerId += 1;\n id = nextQueueOwnerId;\n queueOwners.set(owner, id);\n }\n return `${id}\\0${senderSessionId}\\0${threadId}`;\n};\nexport interface FactoryMessaging {\n controller: FactoryDeliveryController;\n notifications: Pick<NotificationsStorage, 'listNotifications' | 'getNotification'>;\n}\n\nexport interface FactoryMessagingToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'listByOrigin' | 'setOriginMuted'>;\n workItems: Pick<\n WorkItemsStorage,\n | 'findActiveRunBindingForSession'\n | 'getForProject'\n | 'hasRunBindingForSession'\n | 'listSessionSubscriptions'\n | 'listRunBindings'\n | 'listWorkItemSubscribers'\n >;\n auditRecorder: AuditRecorder;\n /** The live controller and notifications store; resolved per call because they exist only once the mount is up. */\n messaging: () => Promise<FactoryMessaging | null>;\n}\n\nconst message = z.string().trim().min(1).max(4000);\n\nfunction senderAttributes(scope: FactoryToolScope) {\n return {\n 'from-session': scope.sessionId,\n 'from-thread': scope.threadId,\n 'from-user': scope.userId,\n };\n}\n\nfunction unavailable(result: Extract<FactoryDeliveryResult, { delivered: 'skipped' }>) {\n return new Error(`The session is not available (${result.reason}).`);\n}\n\nexport function createFactoryMessagingTools(\n scope: FactoryToolScope,\n deps: FactoryMessagingToolDependencies,\n): IntegrationTools {\n const requireMessaging = async () => {\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Session messaging is not available on this server.');\n return messaging;\n };\n // Only a user-session turn the person started is the person's act. Workers\n // (even when someone types into one) and turns nobody signed in started\n // (wakes, webhooks) act as the thread's agent.\n const human = scope.kind === 'user' && scope.interactive;\n const audit = (input: { targetSessionId: string; priority: NotificationPriority; kind: string; delivery: string }) =>\n deps.auditRecorder.record({\n orgId: scope.orgId,\n actorId: human ? scope.userId : `agent:${scope.threadId}`,\n actorType: human ? 'human' : 'agent',\n action: 'factory.agent.signaled',\n factoryProjectId: scope.factoryProjectId,\n targets: [{ type: 'session', id: input.targetSessionId }],\n metadata: {\n targetSessionId: input.targetSessionId,\n priority: input.priority,\n kind: input.kind,\n delivery: input.delivery,\n },\n });\n /** Why this session can't reach `row`, or null when it can: the rule for message targets and reply targets alike. */\n const unreachable = (row: SourceControlSession | null): 'not-found' | 'private' | null => {\n if (!row || row.orgId !== scope.orgId || row.factoryProjectId !== scope.factoryProjectId) return 'not-found';\n if (row.visibility === 'private' && row.userId !== scope.userId) return 'private';\n return null;\n };\n const overRateLimit = (records: NotificationRecord[]) => countedInWindow(records).length >= SIGNAL_RATE_LIMIT;\n /** This session's delivered messages on the thread that still sit inside the window, oldest first. */\n const countedInWindow = (records: NotificationRecord[]): NotificationRecord[] => {\n const since = Date.now() - SIGNAL_RATE_WINDOW_MS;\n return records\n .filter(\n record =>\n // Everything this session sends counts except coalescing progress updates.\n record.kind !== 'progress' &&\n record.kind !== 'stage' &&\n record.attributes?.['from-session'] === scope.sessionId &&\n record.createdAt.getTime() >= since,\n )\n .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());\n };\n /**\n * When the next message can be delivered: one window after the oldest message\n * that has to age out to free a slot, given how many are already waiting.\n */\n const nextDeliveryAt = (counted: NotificationRecord[], waiting: number): number => {\n const blocking = counted[counted.length + waiting - SIGNAL_RATE_LIMIT];\n return (blocking ? blocking.createdAt.getTime() : Date.now()) + SIGNAL_RATE_WINDOW_MS;\n };\n const queuedResult = (deliverAt: number) => ({\n delivered: 'queued' as const,\n deliverAt: new Date(deliverAt).toISOString(),\n note: `Queued; it will be delivered at about ${new Date(deliverAt).toISOString()}. Do not send it again.`,\n });\n const recent = (notifications: FactoryMessaging['notifications'], threadId: string) =>\n notifications.listNotifications({ threadId, source: FACTORY_SESSION_NOTIFICATION_SOURCE, limit: RECENT_SCAN });\n\n const actor = (): QueuedDelivery['actor'] => ({\n actorId: human ? scope.userId : `agent:${scope.threadId}`,\n actorType: human ? 'human' : 'agent',\n });\n\n /**\n * Deliver now, or hold until the window has a slot. The cap limits how fast\n * messages are delivered, not whether they are accepted.\n */\n const accept = async (\n messaging: FactoryMessaging,\n target: { sessionId: string; threadId: string },\n notification: Omit<SendNotificationSignalInput, 'source'>,\n ): Promise<\n { notificationId: string; delivered: FactoryDeliveryResult['delivered'] } | ReturnType<typeof queuedResult>\n > => {\n const counted = countedInWindow(await recent(messaging.notifications, target.threadId));\n if (counted.length < SIGNAL_RATE_LIMIT) {\n const result = await deliverFactorySessionNotification({\n controller: messaging.controller,\n sessions: deps.sessions,\n runs: deps.workItems,\n target,\n notification,\n });\n if (result.delivered === 'skipped') throw unavailable(result);\n await audit({\n targetSessionId: target.sessionId,\n priority: notification.priority ?? 'medium',\n kind: notification.kind,\n delivery: result.delivered,\n });\n return { notificationId: result.notificationId, delivered: result.delivered };\n }\n const key = queueKey(deps, scope.sessionId, target.threadId);\n const waiting = pendingQueues.get(key)?.length ?? 0;\n const deliverAt = nextDeliveryAt(counted, waiting);\n enqueue(key, {\n notification,\n acceptedAt: Date.now(),\n actor: actor(),\n deliverAt,\n target,\n });\n return queuedResult(deliverAt);\n };\n\n const enqueue = (key: string, delivery: QueuedDelivery): void => {\n const queue = pendingQueues.get(key) ?? [];\n queue.push(delivery);\n queue.sort((a, b) => a.acceptedAt - b.acceptedAt);\n pendingQueues.set(key, queue);\n scheduleFlush(key);\n };\n\n /**\n * Deliver the oldest waiting message once the window has a free slot, then\n * the next, in the order they were accepted. A target that is gone by then\n * is dropped and recorded, the same way an undeliverable live message is.\n */\n const flush = async (key: string): Promise<void> => {\n flushTimers.delete(key);\n const queue = pendingQueues.get(key);\n if (!queue || queue.length === 0) {\n pendingQueues.delete(key);\n return;\n }\n const messaging = await deps.messaging();\n if (!messaging) {\n scheduleFlush(key);\n return;\n }\n const head = queue[0];\n if (!head || head.deliverAt > Date.now()) {\n scheduleFlush(key);\n return;\n }\n const counted = countedInWindow(await recent(messaging.notifications, head.target.threadId));\n if (counted.length >= SIGNAL_RATE_LIMIT) {\n scheduleFlush(key);\n return;\n }\n queue.shift();\n if (queue.length === 0) pendingQueues.delete(key);\n const result = await deliverFactorySessionNotification({\n controller: messaging.controller,\n sessions: deps.sessions,\n runs: deps.workItems,\n target: head.target,\n notification: head.notification,\n });\n await deps.auditRecorder.record({\n orgId: scope.orgId,\n actorId: head.actor.actorId,\n actorType: head.actor.actorType,\n action: 'factory.agent.signaled',\n factoryProjectId: scope.factoryProjectId,\n targets: [{ type: 'session', id: head.target.sessionId }],\n metadata: {\n targetSessionId: head.target.sessionId,\n priority: head.notification.priority ?? 'medium',\n kind: head.notification.kind,\n delivery: result.delivered === 'skipped' ? 'dropped' : result.delivered,\n ...(result.delivered === 'skipped' ? { reason: result.reason } : {}),\n },\n });\n if (queue.length > 0) scheduleFlush(key);\n };\n\n function scheduleFlush(key: string): void {\n if (flushTimers.has(key)) return;\n if (!pendingQueues.get(key)?.[0]) return;\n const timer = setTimeout(() => {\n flushTimers.delete(key);\n void flush(key).catch(() => {\n // A failed flush leaves the rest of the queue waiting; the next tick retries the head.\n if ((pendingQueues.get(key)?.length ?? 0) > 0) scheduleFlush(key);\n });\n }, QUEUE_FLUSH_MS);\n // Keep the wait on the real clock. A fake clock that jumps the window would\n // skip a timer scheduled for the old deadline, and the message would sit.\n timer.unref?.();\n flushTimers.set(key, timer);\n }\n\n /** On turns nobody signed in started, only sessions working on items this session follows. */\n const followedWorkers = async () => {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n const follows = await deps.workItems.listSessionSubscriptions({ ...project, sessionId: scope.sessionId });\n const bindings = await Promise.all(\n follows.map(follow => deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, follow.workItemId)),\n );\n const sideSessions = await deps.sessions.listByOrigin({ orgId: scope.orgId, originSessionId: scope.sessionId });\n return new Set([\n ...bindings\n .flat()\n .filter(binding => binding.status === 'active')\n .map(binding => binding.sessionId),\n ...sideSessions.map(session => session.sessionId),\n ]);\n };\n\n const workItemAttribute = async (sessionId: string, orgId: string, factoryProjectId: string) => {\n const binding = await deps.workItems.findActiveRunBindingForSession({ orgId, factoryProjectId, sessionId });\n if (!binding) return {};\n const item = await deps.workItems.getForProject(orgId, factoryProjectId, binding.workItemId);\n if (!item) return {};\n const number = workItemNumber(item);\n return { 'work-item': number === null ? item.id : `#${number}` };\n };\n\n const signal = createTool({\n id: 'factory_signal_session',\n description: [\n 'Send a message to another session in this Factory: a work item role session, or a teammate session you can see.',\n 'Replies come back to this session as `factory-session` notifications with kind `reply` and an `in-reply-to` attribute naming your message.',\n 'Priority `medium` (default) wakes an idle session and waits for a busy one to finish its step; use `urgent` only to make it stop and change course.',\n '`delivered` is `woken` when the session was idle and is now running, `mid-run` when it was already busy and will read the message when its run next checks in.',\n ...(scope.interactive\n ? []\n : ['On this turn you can only message your side sessions and sessions working on items this session follows.']),\n ].join(' '),\n inputSchema: z\n .object({\n sessionId: z.string().trim().min(1).max(200).describe('The session to message.'),\n message: message.describe('What to say. It arrives as-is, so make it self-contained.'),\n priority: z.enum(['low', 'medium', 'high', 'urgent']).default('medium'),\n })\n .strict(),\n execute: async ({ sessionId, message: text, priority }) => {\n if (sessionId === scope.sessionId) throw new Error(\"You can't message your own session.\");\n const row = await deps.sessions.getBySessionId(sessionId);\n const blocked = unreachable(row);\n if (blocked === 'not-found' || !row) throw new Error(`No session ${sessionId} was found in this Factory.`);\n if (blocked === 'private') throw new Error(`Session ${sessionId} is private to its owner.`);\n if (!scope.interactive && !(await followedWorkers()).has(sessionId)) {\n throw new Error(\n `On this turn you can only message this session's side sessions and sessions working on items it follows; ${sessionId} isn't one.`,\n );\n }\n\n const messaging = await requireMessaging();\n const threadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId,\n });\n if (!threadId) throw new Error('The session is not available (thread-not-found).');\n\n return accept(\n messaging,\n { sessionId, threadId },\n {\n kind: 'message',\n priority,\n summary: text,\n payload: { message: text },\n sourceId: scope.sessionId,\n attributes: {\n ...(await workItemAttribute(sessionId, row.orgId, scope.factoryProjectId)),\n ...senderAttributes(scope),\n },\n },\n );\n },\n });\n\n const reply = createTool({\n id: 'factory_message_origin',\n description: [\n 'Answer a message another Factory session sent you, or report on your work.',\n 'With `inReplyTo` (the id of a `factory-session` notification of kind `message`) the answer goes back to the session and thread that sent it; each message can be answered once.',\n 'Without it, the report goes to whoever follows your work: the session that started you, or everyone following your work item.',\n \"Set `status`: `progress` for an update, `blocked` when you need an answer to go on, `done` when finished, `failed` when you can't finish. Send one `done` or `failed` when you finish.\",\n ].join(' '),\n inputSchema: z\n .object({\n message: message.describe('Your answer.'),\n inReplyTo: z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe('Id of the message notification you are answering.'),\n status: z.enum(['progress', 'blocked', 'done', 'failed']).optional(),\n })\n .strict(),\n execute: async ({ message: text, inReplyTo, status }) => {\n if (!inReplyTo) return report(text, status);\n const messaging = await requireMessaging();\n // Only a message that reached this session's own thread can be answered,\n // and the answer goes where that message came from: never to an address\n // the model supplies.\n const original: NotificationRecord | null = await messaging.notifications.getNotification({\n threadId: scope.threadId,\n id: inReplyTo,\n });\n if (!original || original.source !== FACTORY_SESSION_NOTIFICATION_SOURCE) {\n throw new Error(`No message ${inReplyTo} was sent to this session.`);\n }\n if (original.kind !== 'message') {\n throw new Error(`Only messages can be answered; ${inReplyTo} is a ${original.kind}.`);\n }\n const fromSession = original.attributes?.['from-session'];\n const fromThread = original.attributes?.['from-thread'];\n if (typeof fromSession !== 'string' || typeof fromThread !== 'string' || original.sourceId !== fromSession) {\n throw new Error(`Message ${inReplyTo} doesn't say which session sent it.`);\n }\n // Notification records can be written outside these tools, so the\n // sender they name gets the same checks a message to it would.\n const blocked = unreachable(await deps.sessions.getBySessionId(fromSession));\n if (blocked === 'not-found') throw new Error(`Message ${inReplyTo} didn't come from a session in this Factory.`);\n if (blocked === 'private') {\n throw new Error(`Message ${inReplyTo} came from a private session this session can't answer.`);\n }\n const onSenderThread = await recent(messaging.notifications, fromThread);\n // Replies count toward the same cap as messages, so records written to\n // this session can't turn it into an unlimited high-priority sender.\n const answered = onSenderThread.some(\n record =>\n record.kind === 'reply' &&\n record.attributes?.['in-reply-to'] === inReplyTo &&\n record.attributes?.['from-session'] === scope.sessionId,\n );\n if (answered) throw new Error(`You already answered ${inReplyTo}.`);\n\n return accept(\n messaging,\n { sessionId: fromSession, threadId: fromThread },\n {\n kind: 'reply',\n priority: 'high',\n summary: text,\n payload: { message: text },\n sourceId: scope.sessionId,\n attributes: {\n ...senderAttributes(scope),\n 'in-reply-to': inReplyTo,\n ...(status ? { status } : {}),\n },\n },\n );\n },\n });\n\n /** A report with no message to answer: to the origin, else to the item's followers. */\n async function report(text: string, status: 'progress' | 'blocked' | 'done' | 'failed' | undefined) {\n const sender = { sessionId: scope.sessionId, threadId: scope.threadId, userId: scope.userId };\n let recipients: Array<{ sessionId: string; threadId: string }>;\n let attributes: Record<string, string> = {};\n if (scope.origin) {\n // The origin can mute this side session; the session keeps working, its reports just stop.\n // Blocked reports still go through, since the session cannot go on until the origin answers.\n const row = await deps.sessions.getBySessionId(scope.sessionId);\n if (row?.originMutedAt && status !== 'blocked') {\n return { delivered: 0, recipients: [], muted: true, note: 'Your origin muted your reports; keep working.' };\n }\n recipients = [scope.origin];\n } else {\n const binding = await deps.workItems.findActiveRunBindingForSession({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n });\n if (!binding) throw new Error('Nothing started this session; pass inReplyTo to answer a message.');\n recipients = await workItemReportRecipients(deps.workItems, {\n orgId: scope.orgId,\n workItemId: binding.workItemId,\n senderSessionId: scope.sessionId,\n });\n ({ attributes } = await workItemReportAttributes(deps.workItems, {\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n workItemId: binding.workItemId,\n role: binding.role,\n }));\n }\n if (recipients.length === 0) return { delivered: 0, recipients: [] };\n\n const messaging = await requireMessaging();\n const notification = reportNotification({ sender, status, message: text, attributes });\n const results: Array<{\n sessionId: string;\n delivered: FactoryDeliveryResult['delivered'] | 'queued';\n reason?: string;\n deliverAt?: string;\n }> = [];\n for (const recipient of recipients) {\n // Progress replaces itself while unread; anything else can wake a follower, so it counts.\n if (status !== 'progress' && overRateLimit(await recent(messaging.notifications, recipient.threadId))) {\n const counted = countedInWindow(await recent(messaging.notifications, recipient.threadId));\n const key = queueKey(deps, scope.sessionId, recipient.threadId);\n const waiting = pendingQueues.get(key)?.length ?? 0;\n const at = nextDeliveryAt(counted, waiting);\n enqueue(key, {\n notification,\n acceptedAt: Date.now(),\n actor: actor(),\n deliverAt: at,\n target: recipient,\n });\n results.push({\n sessionId: recipient.sessionId,\n delivered: 'queued',\n deliverAt: new Date(at).toISOString(),\n });\n continue;\n }\n const result = await deliverFactorySessionNotification({\n controller: messaging.controller,\n sessions: deps.sessions,\n runs: deps.workItems,\n target: recipient,\n notification,\n });\n results.push({ sessionId: recipient.sessionId, delivered: result.delivered });\n if (result.delivered === 'skipped') continue;\n await audit({\n targetSessionId: recipient.sessionId,\n priority: notification.priority ?? 'high',\n kind: notification.kind,\n delivery: result.delivered,\n });\n }\n const delivered = results.filter(result => result.delivered !== 'skipped' && result.delivered !== 'queued').length;\n const queued = results.filter(result => result.delivered === 'queued').length;\n return {\n delivered,\n recipients: results,\n ...(queued > 0\n ? {\n queued,\n note: 'Queued past the delivery cap. It will be delivered when the window allows. Do not send it again.',\n }\n : {}),\n };\n }\n\n const listSide = createTool({\n id: 'factory_list_side_sessions',\n description: [\n 'List the side sessions this session started with factory_start_session: state (running, parked waiting on an answer, idle), last report, whether you muted it, its model, and the work item it works on.',\n 'Use the session ids with factory_signal_session and factory_switch_session_model. On a turn a person started, also with factory_answer_approval, factory_unsubscribe_session and factory_subscribe_session.',\n ].join(' '),\n inputSchema: z\n .object({\n state: z.enum(['running', 'parked', 'idle']).optional().describe('Only sessions in this state.'),\n muted: z.boolean().optional().describe('true for only muted sessions, false for only unmuted ones.'),\n })\n .strict(),\n execute: async ({ state, muted }) => {\n const messaging = await deps.messaging();\n const sessions = await listSideSessions(\n {\n sessions: deps.sessions,\n workItems: deps.workItems,\n notifications: messaging?.notifications ?? null,\n isRunning: async sessionId => (await messaging?.controller.getSession(sessionId))?.stream.isActive() ?? false,\n model: messaging\n ? sessionId => sessionModel(messaging.controller, deps.sessions, deps.workItems, sessionId)\n : undefined,\n },\n scope,\n );\n return {\n sessions: sessions.filter(\n row => (!state || row.state === state) && (muted === undefined || (row.mutedAt !== null) === muted),\n ),\n };\n },\n });\n\n /** A side session this session started, or an error naming why it isn't one. */\n const ownSideSession = async (sessionId: string) => {\n const row = await deps.sessions.getBySessionId(sessionId);\n if (\n !row ||\n row.orgId !== scope.orgId ||\n row.factoryProjectId !== scope.factoryProjectId ||\n row.originSessionId !== scope.sessionId\n ) {\n throw new Error(`Session ${sessionId} is not a side session you started; see factory_list_side_sessions.`);\n }\n return row;\n };\n const sideSessionId = z\n .string()\n .trim()\n .min(1)\n .max(200)\n .describe('The side session, from factory_list_side_sessions.');\n\n const unsubscribe = createTool({\n id: 'factory_unsubscribe_session',\n description:\n \"Mute a side session you started: its reports and run-end notices stop reaching you, but it keeps working. Approval requests and blocked reports still arrive, since the session can't go on without an answer. Undo with factory_subscribe_session. Only on a turn a person started.\",\n inputSchema: z.object({ sessionId: sideSessionId }).strict(),\n execute: async ({ sessionId }) => {\n requirePerson(scope, 'mute a side session');\n const row = await ownSideSession(sessionId);\n if (row.originMutedAt) return { sessionId, muted: true, mutedAt: row.originMutedAt.toISOString() };\n await deps.sessions.setOriginMuted({ sessionId, muted: true });\n const updated = await deps.sessions.getBySessionId(sessionId);\n return { sessionId, muted: true, mutedAt: updated?.originMutedAt?.toISOString() ?? null };\n },\n });\n\n const subscribe = createTool({\n id: 'factory_subscribe_session',\n description:\n 'Unmute a side session you muted with factory_unsubscribe_session, so its reports reach you again. Reports it sent while muted are not replayed. Only on a turn a person started.',\n inputSchema: z.object({ sessionId: sideSessionId }).strict(),\n execute: async ({ sessionId }) => {\n requirePerson(scope, 'unmute a side session');\n const row = await ownSideSession(sessionId);\n if (row.originMutedAt) await deps.sessions.setOriginMuted({ sessionId, muted: false });\n return { sessionId, muted: false };\n },\n });\n\n if (scope.kind === 'worker') return { factory_message_origin: reply };\n return {\n factory_signal_session: signal,\n factory_message_origin: reply,\n factory_list_side_sessions: listSide,\n factory_unsubscribe_session: unsubscribe,\n factory_subscribe_session: subscribe,\n };\n}\n"],"mappings":"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"}
@@ -21,6 +21,8 @@ export type SessionApprovalReport = (request: {
21
21
  toolCallId: string;
22
22
  args: unknown;
23
23
  }) => Promise<void>;
24
+ /** The provider's own usage-limit text, when the error chain carries one. Status text alone is not enough. */
25
+ export declare function usageLimitReason(error: unknown): string | undefined;
24
26
  /**
25
27
  * Report every run end of a session, however the run started: the board, a
26
28
  * typed message, a wake, or a run after a restart. Attached to each Factory
@@ -1 +1 @@
1
- {"version":3,"file":"run-watch.d.ts","sourceRoot":"","sources":["../../src/tools/run-watch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAG1E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,KAAK,YAAY,GAAG,OAAO,CAAC,oBAAoB,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;AAEnF,wFAAwF;AACxF,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,wFAAwF;AACxF,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE7F,sFAAsF;AACtF,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAKxH;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,mBAAmB,EAC3B,EAAE,mBAAmB,EAAE,GAAE;IAAE,mBAAmB,CAAC,EAAE,qBAAqB,CAAA;CAAO,GAC5E,MAAM,IAAI,CAwCZ;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,oBAAoB,EAC7B,SAAS,EAAE,MAAM,GAChB;IAAE,SAAS,EAAE,MAAM,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,CAqBrD;AAED,yFAAyF;AACzF,wBAAgB,UAAU,CACxB,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAAC,GAAG,SAAS,EAC7D,YAAY,CAAC,EAAE,MAAM,GACpB,iBAAiB,GAAG,IAAI,CAqB1B"}
1
+ {"version":3,"file":"run-watch.d.ts","sourceRoot":"","sources":["../../src/tools/run-watch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAG1E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEtD,KAAK,YAAY,GAAG,OAAO,CAAC,oBAAoB,EAAE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;AAEnF,wFAAwF;AACxF,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,wFAAwF;AACxF,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,iBAAiB,EAAE,KAAK,EAAE,IAAI,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAE7F,sFAAsF;AACtF,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAmBxH,8GAA8G;AAC9G,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAqCnE;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,mBAAmB,EAC3B,EAAE,mBAAmB,EAAE,GAAE;IAAE,mBAAmB,CAAC,EAAE,qBAAqB,CAAA;CAAO,GAC5E,MAAM,IAAI,CAyCZ;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,oBAAoB,EAC7B,SAAS,EAAE,MAAM,GAChB;IAAE,SAAS,EAAE,MAAM,YAAY,CAAC;IAAC,IAAI,EAAE,MAAM,IAAI,CAAA;CAAE,CAqBrD;AAED,yFAAyF;AACzF,wBAAgB,UAAU,CACxB,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAAC,GAAG,SAAS,EAC7D,YAAY,CAAC,EAAE,MAAM,GACpB,iBAAiB,GAAG,IAAI,CAqB1B"}
@@ -3,6 +3,46 @@ import { FACTORY_OPEN_RUN_STALE_MS, heartbeatSessionOpenRun } from "../session/r
3
3
  /** How much of a run's error message a `failed` report quotes. */
4
4
  const ERROR_MESSAGE_LENGTH = 400;
5
5
  /**
6
+ * A usage or quota rejection, not a rate limit or a request-size limit. The
7
+ * wording is the same set account rotation uses, so a provider that reports
8
+ * the cap as a plain 400 still counts.
9
+ */
10
+ const USAGE_LIMIT_MESSAGE_PATTERN = /usage limit|usage cap|insufficient_quota|quota exceeded|quota exhausted|quota reached|exceeded your current quota|insufficient balance|insufficient credits|credits exhausted|rate limit exceeded|rate limit reached|weekly limit|monthly limit/i;
11
+ const ERROR_CHAIN_DEPTH = 5;
12
+ function isRecord(value) {
13
+ return typeof value === "object" && value !== null;
14
+ }
15
+ /** The provider's own usage-limit text, when the error chain carries one. Status text alone is not enough. */
16
+ function usageLimitReason(error) {
17
+ const visited = /* @__PURE__ */ new Set();
18
+ const found = [];
19
+ const add = (value) => {
20
+ if (typeof value !== "string") return;
21
+ const text = value.trim();
22
+ if (text && USAGE_LIMIT_MESSAGE_PATTERN.test(text)) found.push(text);
23
+ };
24
+ function walk(value, depth) {
25
+ if (depth > ERROR_CHAIN_DEPTH || !isRecord(value) || visited.has(value)) return;
26
+ visited.add(value);
27
+ add(value.message);
28
+ if (typeof value.responseBody === "string") try {
29
+ const body = JSON.parse(value.responseBody);
30
+ if (isRecord(body)) {
31
+ add(body.message);
32
+ add(body.detail);
33
+ if (isRecord(body.error)) add(body.error.message);
34
+ else add(body.error);
35
+ }
36
+ } catch {}
37
+ if (value.cause !== void 0) walk(value.cause, depth + 1);
38
+ if (value.error !== void 0) walk(value.error, depth + 1);
39
+ }
40
+ try {
41
+ walk(error, 0);
42
+ } catch {}
43
+ return (found.find((text) => text !== "Bad Request") ?? found[0])?.slice(0, ERROR_MESSAGE_LENGTH);
44
+ }
45
+ /**
6
46
  * Report every run end of a session, however the run started: the board, a
7
47
  * typed message, a wake, or a run after a restart. Attached to each Factory
8
48
  * session when it is created, so it outlives any one start. A run someone
@@ -31,7 +71,7 @@ function observeSessionRunReports(session, report, { onApprovalRequested } = {})
31
71
  return;
32
72
  }
33
73
  if (event.type === "error") {
34
- lastError = event.error?.message;
74
+ lastError = usageLimitReason(event.error) ?? event.error?.message;
35
75
  return;
36
76
  }
37
77
  if (event.type === "tool_suspension_cancelled" && parked?.toolCallId === event.toolCallId) {
@@ -97,6 +137,6 @@ function runOutcome(reason, parked, errorMessage) {
97
137
  }
98
138
  }
99
139
  //#endregion
100
- export { heartbeatOpenRunUntilEnd, observeSessionRunReports, runOutcome };
140
+ export { heartbeatOpenRunUntilEnd, observeSessionRunReports, runOutcome, usageLimitReason };
101
141
 
102
142
  //# sourceMappingURL=run-watch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"run-watch.js","names":[],"sources":["../../src/tools/run-watch.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\n\nimport { FACTORY_OPEN_RUN_STALE_MS, heartbeatSessionOpenRun } from '../session/run-audit.js';\nimport type { RunEndCaptureSession } from '../session/run-audit.js';\nimport type { FactoryRunOutcome } from './reports.js';\n\ntype RunEndReason = Extract<AgentControllerEvent, { type: 'agent_end' }>['reason'];\n\n/** What a run stopped on: a tool waiting for an answer, or one waiting for approval. */\nexport interface ParkedTool {\n toolName: string;\n toolCallId: string;\n approval: boolean;\n}\n\nexport interface RunReportSession {\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\n/** Reports one run's end; `since` is when the run's window opened. Should not throw. */\nexport type SessionRunEndReport = (outcome: FactoryRunOutcome, since: Date) => Promise<void>;\n\n/** Told when the run holds a tool call for approval; the run stays open meanwhile. */\nexport type SessionApprovalReport = (request: { toolName: string; toolCallId: string; args: unknown }) => Promise<void>;\n\n/** How much of a run's error message a `failed` report quotes. */\nconst ERROR_MESSAGE_LENGTH = 400;\n\n/**\n * Report every run end of a session, however the run started: the board, a\n * typed message, a wake, or a run after a restart. Attached to each Factory\n * session when it is created, so it outlives any one start. A run someone\n * stopped or steered says nothing, and its window carries into the next run,\n * so reports the agent made in it still count.\n */\nexport function observeSessionRunReports(\n session: RunReportSession,\n report: SessionRunEndReport,\n { onApprovalRequested }: { onApprovalRequested?: SessionApprovalReport } = {},\n): () => void {\n let parked: ParkedTool | undefined;\n let lastError: string | undefined;\n let since = new Date();\n const settle = (work: Promise<void>) =>\n void work.catch(error => {\n console.warn('[Factory reports] Unable to report on a session run.', error);\n });\n return session.subscribe(event => {\n if (event.type === 'tool_suspended' || event.type === 'tool_approval_required') {\n parked = {\n toolName: event.toolName,\n toolCallId: event.toolCallId,\n approval: event.type === 'tool_approval_required',\n };\n // An approval gate holds the run open, so it is announced now, not at the run's end.\n if (event.type === 'tool_approval_required' && onApprovalRequested) {\n settle(onApprovalRequested({ toolName: event.toolName, toolCallId: event.toolCallId, args: event.args }));\n }\n return;\n }\n if (event.type === 'error') {\n lastError = event.error?.message;\n return;\n }\n if (event.type === 'tool_suspension_cancelled' && parked?.toolCallId === event.toolCallId) {\n parked = undefined;\n return;\n }\n if (event.type !== 'agent_end') return;\n const outcome = runOutcome(event.reason, parked, lastError);\n parked = undefined;\n lastError = undefined;\n if (!outcome) return;\n // Move the window now, so a run that starts before this report settles is judged on its own;\n // reports made up to the end, including its last millisecond, belong to the run that ended.\n const runSince = since;\n since = new Date(Date.now() + 1);\n settle(report(outcome, runSince));\n });\n}\n\n/**\n * Renew a kicked-off run's open-run entry until the run ends, like the\n * dispatcher's heartbeat, so no dispatcher starts the role again meanwhile.\n * `endReason` is how that run ended, once it has; `stop` is for a kickoff that fails.\n */\nexport function heartbeatOpenRunUntilEnd(\n session: RunEndCaptureSession,\n kickoffId: string,\n): { endReason: () => RunEndReason; stop: () => void } {\n let endReason: RunEndReason;\n let unsubscribe = () => {};\n const timer = setInterval(\n () => void heartbeatSessionOpenRun(session, kickoffId),\n Math.floor(FACTORY_OPEN_RUN_STALE_MS / 3),\n );\n timer.unref?.();\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n unsubscribe();\n };\n unsubscribe = session.subscribe(event => {\n if (event.type !== 'agent_end') return;\n endReason ??= event.reason;\n stop();\n });\n return { endReason: () => endReason, stop };\n}\n\n/** `null` for an aborted run: someone stopped or steered it, which is not an outcome. */\nexport function runOutcome(\n reason: RunEndReason,\n parked: Pick<ParkedTool, 'toolName' | 'approval'> | undefined,\n errorMessage?: string,\n): FactoryRunOutcome | null {\n switch (reason) {\n case 'error': {\n const message = errorMessage?.trim();\n return {\n status: 'failed',\n reason: message\n ? `the run ended in error: ${message.slice(0, ERROR_MESSAGE_LENGTH)}`\n : 'the run ended in error.',\n };\n }\n case 'aborted':\n return null;\n case 'suspended':\n return {\n status: 'blocked',\n ...(parked ? { toolName: parked.toolName, ...(parked.approval ? { approval: true } : {}) } : {}),\n };\n default:\n return { status: 'done' };\n }\n}\n"],"mappings":";;;AA0BA,MAAM,uBAAuB;;;;;;;;AAS7B,SAAgB,yBACd,SACA,QACA,EAAE,wBAAyE,CAAC,GAChE;CACZ,IAAI;CACJ,IAAI;CACJ,IAAI,wBAAQ,IAAI,KAAK;CACrB,MAAM,UAAU,SACd,KAAK,KAAK,OAAM,UAAS;EACvB,QAAQ,KAAK,wDAAwD,KAAK;CAC5E,CAAC;CACH,OAAO,QAAQ,WAAU,UAAS;EAChC,IAAI,MAAM,SAAS,oBAAoB,MAAM,SAAS,0BAA0B;GAC9E,SAAS;IACP,UAAU,MAAM;IAChB,YAAY,MAAM;IAClB,UAAU,MAAM,SAAS;GAC3B;GAEA,IAAI,MAAM,SAAS,4BAA4B,qBAC7C,OAAO,oBAAoB;IAAE,UAAU,MAAM;IAAU,YAAY,MAAM;IAAY,MAAM,MAAM;GAAK,CAAC,CAAC;GAE1G;EACF;EACA,IAAI,MAAM,SAAS,SAAS;GAC1B,YAAY,MAAM,OAAO;GACzB;EACF;EACA,IAAI,MAAM,SAAS,+BAA+B,QAAQ,eAAe,MAAM,YAAY;GACzF,SAAS,KAAA;GACT;EACF;EACA,IAAI,MAAM,SAAS,aAAa;EAChC,MAAM,UAAU,WAAW,MAAM,QAAQ,QAAQ,SAAS;EAC1D,SAAS,KAAA;EACT,YAAY,KAAA;EACZ,IAAI,CAAC,SAAS;EAGd,MAAM,WAAW;EACjB,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC;EAC/B,OAAO,OAAO,SAAS,QAAQ,CAAC;CAClC,CAAC;AACH;;;;;;AAOA,SAAgB,yBACd,SACA,WACqD;CACrD,IAAI;CACJ,IAAI,oBAAoB,CAAC;CACzB,MAAM,QAAQ,kBACN,KAAK,wBAAwB,SAAS,SAAS,GACrD,KAAK,MAAM,4BAA4B,CAAC,CAC1C;CACA,MAAM,QAAQ;CACd,IAAI,UAAU;CACd,MAAM,aAAa;EACjB,IAAI,SAAS;EACb,UAAU;EACV,cAAc,KAAK;EACnB,YAAY;CACd;CACA,cAAc,QAAQ,WAAU,UAAS;EACvC,IAAI,MAAM,SAAS,aAAa;EAChC,cAAc,MAAM;EACpB,KAAK;CACP,CAAC;CACD,OAAO;EAAE,iBAAiB;EAAW;CAAK;AAC5C;;AAGA,SAAgB,WACd,QACA,QACA,cAC0B;CAC1B,QAAQ,QAAR;EACE,KAAK,SAAS;GACZ,MAAM,UAAU,cAAc,KAAK;GACnC,OAAO;IACL,QAAQ;IACR,QAAQ,UACJ,2BAA2B,QAAQ,MAAM,GAAG,oBAAoB,MAChE;GACN;EACF;EACA,KAAK,WACH,OAAO;EACT,KAAK,aACH,OAAO;GACL,QAAQ;GACR,GAAI,SAAS;IAAE,UAAU,OAAO;IAAU,GAAI,OAAO,WAAW,EAAE,UAAU,KAAK,IAAI,CAAC;GAAG,IAAI,CAAC;EAChG;EACF,SACE,OAAO,EAAE,QAAQ,OAAO;CAC5B;AACF"}
1
+ {"version":3,"file":"run-watch.js","names":[],"sources":["../../src/tools/run-watch.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\n\nimport { FACTORY_OPEN_RUN_STALE_MS, heartbeatSessionOpenRun } from '../session/run-audit.js';\nimport type { RunEndCaptureSession } from '../session/run-audit.js';\nimport type { FactoryRunOutcome } from './reports.js';\n\ntype RunEndReason = Extract<AgentControllerEvent, { type: 'agent_end' }>['reason'];\n\n/** What a run stopped on: a tool waiting for an answer, or one waiting for approval. */\nexport interface ParkedTool {\n toolName: string;\n toolCallId: string;\n approval: boolean;\n}\n\nexport interface RunReportSession {\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\n/** Reports one run's end; `since` is when the run's window opened. Should not throw. */\nexport type SessionRunEndReport = (outcome: FactoryRunOutcome, since: Date) => Promise<void>;\n\n/** Told when the run holds a tool call for approval; the run stays open meanwhile. */\nexport type SessionApprovalReport = (request: { toolName: string; toolCallId: string; args: unknown }) => Promise<void>;\n\n/** How much of a run's error message a `failed` report quotes. */\nconst ERROR_MESSAGE_LENGTH = 400;\n\n/**\n * A usage or quota rejection, not a rate limit or a request-size limit. The\n * wording is the same set account rotation uses, so a provider that reports\n * the cap as a plain 400 still counts.\n */\nconst USAGE_LIMIT_MESSAGE_PATTERN =\n /usage limit|usage cap|insufficient_quota|quota exceeded|quota exhausted|quota reached|exceeded your current quota|insufficient balance|insufficient credits|credits exhausted|rate limit exceeded|rate limit reached|weekly limit|monthly limit/i;\n\nconst ERROR_CHAIN_DEPTH = 5;\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\n/** The provider's own usage-limit text, when the error chain carries one. Status text alone is not enough. */\nexport function usageLimitReason(error: unknown): string | undefined {\n const visited = new Set<object>();\n const found: string[] = [];\n const add = (value: unknown) => {\n if (typeof value !== 'string') return;\n const text = value.trim();\n if (text && USAGE_LIMIT_MESSAGE_PATTERN.test(text)) found.push(text);\n };\n\n function walk(value: unknown, depth: number) {\n if (depth > ERROR_CHAIN_DEPTH || !isRecord(value) || visited.has(value)) return;\n visited.add(value);\n add(value.message);\n if (typeof value.responseBody === 'string') {\n try {\n const body: unknown = JSON.parse(value.responseBody);\n if (isRecord(body)) {\n add(body.message);\n add(body.detail);\n if (isRecord(body.error)) add(body.error.message);\n else add(body.error);\n }\n } catch {\n // A non-JSON body can echo the request. Leave it out.\n }\n }\n if (value.cause !== undefined) walk(value.cause, depth + 1);\n if (value.error !== undefined) walk(value.error, depth + 1);\n }\n\n try {\n walk(error, 0);\n } catch {\n // A malformed error object still reports whatever was already found.\n }\n const reason = found.find(text => text !== 'Bad Request') ?? found[0];\n return reason?.slice(0, ERROR_MESSAGE_LENGTH);\n}\n\n/**\n * Report every run end of a session, however the run started: the board, a\n * typed message, a wake, or a run after a restart. Attached to each Factory\n * session when it is created, so it outlives any one start. A run someone\n * stopped or steered says nothing, and its window carries into the next run,\n * so reports the agent made in it still count.\n */\nexport function observeSessionRunReports(\n session: RunReportSession,\n report: SessionRunEndReport,\n { onApprovalRequested }: { onApprovalRequested?: SessionApprovalReport } = {},\n): () => void {\n let parked: ParkedTool | undefined;\n let lastError: string | undefined;\n let since = new Date();\n const settle = (work: Promise<void>) =>\n void work.catch(error => {\n console.warn('[Factory reports] Unable to report on a session run.', error);\n });\n return session.subscribe(event => {\n if (event.type === 'tool_suspended' || event.type === 'tool_approval_required') {\n parked = {\n toolName: event.toolName,\n toolCallId: event.toolCallId,\n approval: event.type === 'tool_approval_required',\n };\n // An approval gate holds the run open, so it is announced now, not at the run's end.\n if (event.type === 'tool_approval_required' && onApprovalRequested) {\n settle(onApprovalRequested({ toolName: event.toolName, toolCallId: event.toolCallId, args: event.args }));\n }\n return;\n }\n if (event.type === 'error') {\n const quota = usageLimitReason(event.error);\n lastError = quota ?? event.error?.message;\n return;\n }\n if (event.type === 'tool_suspension_cancelled' && parked?.toolCallId === event.toolCallId) {\n parked = undefined;\n return;\n }\n if (event.type !== 'agent_end') return;\n const outcome = runOutcome(event.reason, parked, lastError);\n parked = undefined;\n lastError = undefined;\n if (!outcome) return;\n // Move the window now, so a run that starts before this report settles is judged on its own;\n // reports made up to the end, including its last millisecond, belong to the run that ended.\n const runSince = since;\n since = new Date(Date.now() + 1);\n settle(report(outcome, runSince));\n });\n}\n\n/**\n * Renew a kicked-off run's open-run entry until the run ends, like the\n * dispatcher's heartbeat, so no dispatcher starts the role again meanwhile.\n * `endReason` is how that run ended, once it has; `stop` is for a kickoff that fails.\n */\nexport function heartbeatOpenRunUntilEnd(\n session: RunEndCaptureSession,\n kickoffId: string,\n): { endReason: () => RunEndReason; stop: () => void } {\n let endReason: RunEndReason;\n let unsubscribe = () => {};\n const timer = setInterval(\n () => void heartbeatSessionOpenRun(session, kickoffId),\n Math.floor(FACTORY_OPEN_RUN_STALE_MS / 3),\n );\n timer.unref?.();\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n clearInterval(timer);\n unsubscribe();\n };\n unsubscribe = session.subscribe(event => {\n if (event.type !== 'agent_end') return;\n endReason ??= event.reason;\n stop();\n });\n return { endReason: () => endReason, stop };\n}\n\n/** `null` for an aborted run: someone stopped or steered it, which is not an outcome. */\nexport function runOutcome(\n reason: RunEndReason,\n parked: Pick<ParkedTool, 'toolName' | 'approval'> | undefined,\n errorMessage?: string,\n): FactoryRunOutcome | null {\n switch (reason) {\n case 'error': {\n const message = errorMessage?.trim();\n return {\n status: 'failed',\n reason: message\n ? `the run ended in error: ${message.slice(0, ERROR_MESSAGE_LENGTH)}`\n : 'the run ended in error.',\n };\n }\n case 'aborted':\n return null;\n case 'suspended':\n return {\n status: 'blocked',\n ...(parked ? { toolName: parked.toolName, ...(parked.approval ? { approval: true } : {}) } : {}),\n };\n default:\n return { status: 'done' };\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"session-tools.d.ts","sourceRoot":"","sources":["../../src/tools/session-tools.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAKhE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE7E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAQhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAElF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAInD,sGAAsG;AACtG,eAAO,MAAM,sBAAsB,QAAoD,CAAC;AAExF,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,6BAA6B,CAAC,CAAC;IAC7F,YAAY,EAAE;QACZ,QAAQ,EAAE,sBAAsB,CAAC;QACjC,aAAa,EAAE,oBAAoB,CAAC;QACpC;;;;WAIG;QACH,aAAa,CAAC,EAAE;YACd,WAAW,EAAE,IAAI,CAAC,uBAAuB,EAAE,iBAAiB,CAAC,CAAC;YAC9D,eAAe,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,aAAa,GAAG,MAAM,CAAC,CAAC;YACvE,mBAAmB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;SAC3C,CAAC;KACH,CAAC;IACF,wEAAwE;IACxE,SAAS,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAClD,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,MAAM,GACN,eAAe,GACf,iBAAiB,GACjB,gCAAgC,GAChC,wBAAwB,GACxB,yBAAyB,GACzB,qBAAqB,GACrB,yBAAyB,GACzB,0BAA0B,GAC1B,yBAAyB,CAC5B,CAAC;IACF,MAAM,EAAE,aAAa,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IAC1C,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC;CAC9E;AA0BD,oGAAoG;AACpG,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,8BAA8B,GACnC,gBAAgB,CAsYlB"}
1
+ {"version":3,"file":"session-tools.d.ts","sourceRoot":"","sources":["../../src/tools/session-tools.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAKhE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE7E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAQhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAElF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnD,sGAAsG;AACtG,eAAO,MAAM,sBAAsB,QAAoD,CAAC;AAExF,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,6BAA6B,CAAC,CAAC;IAC7F,YAAY,EAAE;QACZ,QAAQ,EAAE,sBAAsB,CAAC;QACjC,aAAa,EAAE,oBAAoB,CAAC;QACpC;;;;WAIG;QACH,aAAa,CAAC,EAAE;YACd,WAAW,EAAE,IAAI,CAAC,uBAAuB,EAAE,iBAAiB,CAAC,CAAC;YAC9D,eAAe,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,aAAa,GAAG,MAAM,CAAC,CAAC;YACvE,mBAAmB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;SAC3C,CAAC;KACH,CAAC;IACF,wEAAwE;IACxE,SAAS,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAClD,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,MAAM,GACN,eAAe,GACf,iBAAiB,GACjB,gCAAgC,GAChC,wBAAwB,GACxB,yBAAyB,GACzB,qBAAqB,GACrB,yBAAyB,GACzB,0BAA0B,GAC1B,yBAAyB,CAC5B,CAAC;IACF,MAAM,EAAE,aAAa,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IAC1C,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC;CAC9E;AA0BD,oGAAoG;AACpG,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,8BAA8B,GACnC,gBAAgB,CA4alB"}
@@ -5,6 +5,7 @@ import { effectiveModelScope } from "../storage/domains/projects/base.js";
5
5
  import { ensureFactorySourceSession, hydrateFactorySession, resolveFactorySessionModel } from "../session/factory-session.js";
6
6
  import { resolveSessionThread } from "./deliver.js";
7
7
  import { findWorkItem, workItemNotFound, workItemNumber, workItemRefSchema } from "./read-tools.js";
8
+ import { sessionModel } from "./side-sessions.js";
8
9
  import { resolveFactoryModelPolicy } from "../session/credential-policy.js";
9
10
  import { buildModelCatalog, listSessionScopedCredentials } from "../routes/config.js";
10
11
  import { USER_SESSION_BRANCH_PREFIX } from "../routes/source-control-sessions.js";
@@ -87,6 +88,10 @@ function createFactorySessionTools(scope, deps) {
87
88
  scopeLabel: modelScope === "factory" ? "Factory" : "session owner"
88
89
  };
89
90
  }
91
+ function unrunnableModelError(modelId, catalog) {
92
+ const options = catalog.usable.length ? ` Models available to it: ${catalog.usable.slice(0, LISTED_MODELS).join(", ")}${catalog.usable.length > LISTED_MODELS ? ", …" : ""}.` : " No models are available to it; connect a provider credential first.";
93
+ return /* @__PURE__ */ new Error(catalog.known.has(modelId) ? `Model ${modelId} exists, but this session's credential scope (${catalog.scopeLabel}) has no credential for its provider.${options}` : `Unknown model ${modelId}.${options}`);
94
+ }
90
95
  const switchModel = createTool({
91
96
  id: "factory_switch_session_model",
92
97
  description: "Switch the model of a session you own in this Factory (side, work item, or review session) without recreating it. A running session switches immediately; a stopped one uses the model on its next run. The model must be one the session credentials can run: the owner credentials for a user-scoped session, the Factory credentials for a Factory-scoped one. Use a session ID from the Factory session or work item tools.",
@@ -109,10 +114,7 @@ function createFactorySessionTools(scope, deps) {
109
114
  if (!threadId) throw new Error("Session has no conversation thread yet.");
110
115
  const resourceId = (await messaging.controller.queryThreadById({ threadId }))?.resourceId ?? sessionId;
111
116
  const catalog = await sessionModelCatalog(messaging.controller, sessionId, threadId, resourceId);
112
- if (!catalog.usable.includes(modelId)) {
113
- const options = catalog.usable.length ? ` Models available to it: ${catalog.usable.slice(0, LISTED_MODELS).join(", ")}${catalog.usable.length > LISTED_MODELS ? ", …" : ""}.` : " No models are available to it; connect a provider credential first.";
114
- throw new Error(catalog.known.has(modelId) ? `Model ${modelId} exists, but this session's credential scope (${catalog.scopeLabel}) has no credential for its provider.${options}` : `Unknown model ${modelId}.${options}`);
115
- }
117
+ if (!catalog.usable.includes(modelId)) throw unrunnableModelError(modelId, catalog);
116
118
  const { live } = await messaging.controller.setThreadModel({
117
119
  resourceId,
118
120
  threadId,
@@ -130,6 +132,7 @@ function createFactorySessionTools(scope, deps) {
130
132
  id: "factory_start_session",
131
133
  description: [
132
134
  "Start a side session in this Factory with a prompt. It runs on its own sandbox and branch as you, and reports back to this session with `factory-session` notifications.",
135
+ "A side session uses this session’s model. Pass `model` to start it on another model the session credentials can run.",
133
136
  "Use it for self-contained investigations; when the result should land as a pull request, file a work item instead.",
134
137
  "With `workItem` and `role` it starts that role on the work item instead, without moving the card, and this session follows the item.",
135
138
  "A role that already has a session on the item (current or past) can't be started again from here: message that session with factory_signal_session, or ask the person to use the board."
@@ -137,13 +140,14 @@ function createFactorySessionTools(scope, deps) {
137
140
  inputSchema: z.object({
138
141
  prompt: z.string().trim().min(1).max(8e3).describe("The first message the new session receives."),
139
142
  title: z.string().trim().min(1).max(TITLE_LENGTH).optional(),
143
+ model: z.string().trim().min(1).max(200).optional().describe("Model id for a side session. Omit it to inherit this session’s model. The session credentials must be able to run it."),
140
144
  workItem: workItemRefSchema.optional(),
141
145
  role: z.string().trim().min(1).max(64).optional()
142
146
  }).strict().refine((input) => input.role === void 0 || input.workItem !== void 0, {
143
147
  message: "Pass workItem together with role.",
144
148
  path: ["role"]
145
149
  }),
146
- execute: async ({ prompt, title, workItem, role }) => {
150
+ execute: async ({ prompt, title, model, workItem, role }) => {
147
151
  if (workItem !== void 0) {
148
152
  if (!scope.interactive) throw new Error("Starting a role on a work item needs a person in this turn; start a side session, or ask the person to use the board.");
149
153
  return startWorkItemRole({
@@ -158,6 +162,17 @@ function createFactorySessionTools(scope, deps) {
158
162
  if (!parent || !sourceControl) throw new Error("This session has no Factory session row to start from.");
159
163
  const messaging = await deps.messaging();
160
164
  if (!messaging) throw new Error("Starting sessions is not available on this server.");
165
+ const parentThreadId = await resolveSessionThread({
166
+ controller: messaging.controller,
167
+ sessions: deps.sessions,
168
+ workItems: deps.workItems,
169
+ sessionId: scope.sessionId
170
+ });
171
+ const parentResourceId = parentThreadId ? (await messaging.controller.queryThreadById({ threadId: parentThreadId }))?.resourceId ?? scope.sessionId : scope.sessionId;
172
+ const catalog = await sessionModelCatalog(messaging.controller, scope.sessionId, parentThreadId ?? scope.threadId, parentResourceId);
173
+ const inherited = model ? null : await sessionModel(messaging.controller, deps.sessions, deps.workItems, scope.sessionId);
174
+ const requestedModel = model ?? (inherited && catalog.usable.includes(inherited) ? inherited : void 0);
175
+ if (requestedModel && !catalog.usable.includes(requestedModel)) throw unrunnableModelError(requestedModel, catalog);
161
176
  const sessionId = globalThis.crypto.randomUUID();
162
177
  const ensured = await ensureFactorySourceSession({
163
178
  sourceControl,
@@ -173,7 +188,7 @@ function createFactorySessionTools(scope, deps) {
173
188
  threadId: scope.threadId
174
189
  }
175
190
  });
176
- const sessionModel = await resolveFactorySessionModel({
191
+ const resolvedModel = await resolveFactorySessionModel({
177
192
  ...deps.sessionModel,
178
193
  factoryProjectId: scope.factoryProjectId,
179
194
  orgId: scope.orgId,
@@ -200,9 +215,9 @@ function createFactorySessionTools(scope, deps) {
200
215
  await session.state.set(tags);
201
216
  await hydrateFactorySession(session, {
202
217
  orgId: scope.orgId,
203
- modelScope: sessionModel.modelScope,
218
+ modelScope: resolvedModel.modelScope,
204
219
  ownerUserId: scope.userId,
205
- defaultModelId: sessionModel.modelId
220
+ defaultModelId: requestedModel ?? resolvedModel.modelId
206
221
  });
207
222
  const threadId = session.thread.requireId();
208
223
  const threadTitle = title ?? titleFrom(prompt);
@@ -221,7 +236,7 @@ function createFactorySessionTools(scope, deps) {
221
236
  await primeTenantCredentialsForTenant({
222
237
  orgId: scope.orgId,
223
238
  userId: scope.userId,
224
- credentialScope: sessionModel.modelScope
239
+ credentialScope: resolvedModel.modelScope
225
240
  });
226
241
  await session.sendSignal({
227
242
  id: globalThis.crypto.randomUUID(),
@@ -1 +1 @@
1
- {"version":3,"file":"session-tools.js","names":[],"sources":["../../src/tools/session-tools.ts"],"sourcesContent":["import { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/registry.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { buildModelCatalog, listSessionScopedCredentials } from '../routes/config.js';\nimport { USER_SESSION_BRANCH_PREFIX } from '../routes/source-control-sessions.js';\nimport { primeTenantCredentialsForTenant } from '../routes/tenant-credentials.js';\nimport { FactoryStartTransitionError, WorkItemRepositoryError } from '../rules/start-coordinator.js';\nimport type { FactoryStartCoordinator } from '../rules/start-coordinator.js';\nimport { resolveFactoryModelPolicy } from '../session/credential-policy.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport {\n ensureFactorySourceSession,\n hydrateFactorySession,\n resolveFactorySessionModel,\n} from '../session/factory-session.js';\nimport { recordSessionRunStart } from '../session/run-audit.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { ModelCredentialsStorage } from '../storage/domains/credentials/base.js';\nimport type { CustomProvidersStorage } from '../storage/domains/custom-providers/base.js';\nimport type { IntakeStorage } from '../storage/domains/intake/base.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { deliverFactorySessionNotification, resolveSessionThread } from './deliver.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport { findWorkItem, workItemNotFound, workItemNumber, workItemRefSchema } from './read-tools.js';\nimport { heartbeatOpenRunUntilEnd } from './run-watch.js';\nimport type { FactoryToolScope } from './scope.js';\n\nconst TITLE_LENGTH = 120;\n\n/** Who renews the open-run entries this process writes; one id per process, like the dispatcher's. */\nexport const FACTORY_TOOLS_OWNER_ID = `factory-tools:${globalThis.crypto.randomUUID()}`;\n\nexport interface FactorySessionToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'getSourceControlBySessionId'>;\n sessionModel: {\n projects: FactoryProjectsStorage;\n modelDefaults: ModelDefaultsStorage;\n /**\n * Tenant credential stores, present when auth is on. Model switches then\n * check the target session's credential scope, like the Settings picker;\n * without them the server's own keys decide.\n */\n tenantCatalog?: {\n credentials: Pick<ModelCredentialsStorage, 'listCredentials'>;\n customProviders?: Pick<CustomProvidersStorage, 'ensureReady' | 'list'>;\n deploymentProviders?: ReadonlySet<string>;\n };\n };\n /** Resolved lazily: the controller only exists once the mount is up. */\n messaging: () => Promise<FactoryMessaging | null>;\n workItems: Pick<\n WorkItemsStorage,\n | 'list'\n | 'getForProject'\n | 'listRunBindings'\n | 'findActiveRunBindingForSession'\n | 'findRunBindingByThread'\n | 'hasRunBindingForSession'\n | 'subscribeToWorkItem'\n | 'unsubscribeFromWorkItem'\n | 'listSessionSubscriptions'\n | 'listWorkItemSubscribers'\n >;\n boards: BoardRegistry;\n auditRecorder: AuditRecorder;\n intake?: Pick<IntakeStorage, 'getConfig'>;\n /** The browser Start's coordinator; absent until the Factory runtime is ready. */\n startCoordinator: () => Pick<FactoryStartCoordinator, 'prepare'> | undefined;\n}\n\nconst LISTED_MODELS = 8;\n\nfunction ambiguousRepository(candidates: readonly string[]): string {\n return `This item has several repositories (${candidates.join(', ')}); v1 can't choose one.`;\n}\n\nfunction startErrorMessage(error: unknown): string | null {\n if (error instanceof WorkItemRepositoryError) {\n return error.code === 'repository_ambiguous' ? ambiguousRepository(error.candidates) : error.message;\n }\n if (error instanceof FactoryStartTransitionError) return `The start was refused: ${error.result.reason}`;\n return null;\n}\n\nfunction workItemLabel(item: WorkItemRow): string {\n const number = workItemNumber(item);\n return number === null ? `\"${item.title}\"` : `#${number}`;\n}\n\nfunction titleFrom(prompt: string): string {\n const firstLine = prompt.split('\\n').find(line => line.trim()) ?? prompt;\n return firstLine.trim().slice(0, TITLE_LENGTH);\n}\n\n/** Tools that start sessions on the caller's behalf, or switch their models. User sessions only. */\nexport function createFactorySessionTools(\n scope: FactoryToolScope,\n deps: FactorySessionToolDependencies,\n): IntegrationTools {\n /** The models a session can switch to, resolved from the target session's owner and project policy. */\n async function sessionModelCatalog(\n controller: FactoryMessaging['controller'],\n sessionId: string,\n threadId: string,\n resourceId: string,\n ): Promise<{ usable: string[]; known: Set<string>; scopeLabel: string }> {\n const models = await controller.listAvailableModels();\n const known = new Set(models.flatMap(model => (typeof model.id === 'string' ? [model.id] : [])));\n const tenantCatalog = deps.sessionModel.tenantCatalog;\n if (!tenantCatalog) {\n const usable = (await buildModelCatalog({ controller })).map(model => model.id);\n return { usable, known, scopeLabel: 'server' };\n }\n const requestContext = new RequestContext();\n requestContext.set('user', { id: scope.userId, organizationId: scope.orgId });\n requestContext.set('controller', { sessionId, resourceId, threadId });\n const policy = await resolveFactoryModelPolicy(requestContext, {\n projects: deps.sessionModel.projects,\n sourceControl: deps.sessions,\n workItems: deps.workItems,\n });\n if (!policy) throw new Error('Session model policy is unavailable.');\n const modelScope = policy.tenant.credentialScope ?? 'user';\n const orgId = policy.tenant.orgId ?? scope.orgId;\n const customProviders = tenantCatalog.customProviders;\n const usable = (\n await buildModelCatalog({\n controller,\n tenantCredentials: await listSessionScopedCredentials({\n credentials: tenantCatalog.credentials,\n orgId,\n userId: policy.tenant.userId,\n modelScope,\n }),\n deploymentProviders: tenantCatalog.deploymentProviders,\n listCustomProviders: customProviders\n ? async () => {\n await customProviders.ensureReady();\n return customProviders.list({ orgId });\n }\n : undefined,\n })\n ).map(model => model.id);\n for (const id of usable) known.add(id);\n return { usable, known, scopeLabel: modelScope === 'factory' ? 'Factory' : 'session owner' };\n }\n\n const switchModel = createTool({\n id: 'factory_switch_session_model',\n description:\n 'Switch the model of a session you own in this Factory (side, work item, or review session) without recreating it. A running session switches immediately; a stopped one uses the model on its next run. The model must be one the session credentials can run: the owner credentials for a user-scoped session, the Factory credentials for a Factory-scoped one. Use a session ID from the Factory session or work item tools.',\n inputSchema: z\n .object({\n sessionId: z.string().trim().min(1).max(200),\n modelId: z.string().trim().min(1).max(200),\n })\n .strict(),\n execute: async ({ sessionId, modelId }) => {\n const row = await deps.sessions.getBySessionId(sessionId);\n if (\n !row ||\n row.orgId !== scope.orgId ||\n row.factoryProjectId !== scope.factoryProjectId ||\n row.userId !== scope.userId\n ) {\n throw new Error('Session not found or not owned by you in this Factory.');\n }\n // A turn nobody signed in started reaches only this session and the side sessions it started.\n if (!scope.interactive && sessionId !== scope.sessionId && row.originSessionId !== scope.sessionId) {\n throw new Error(\n 'Without a person in this turn, only this session and the side sessions it started can switch models.',\n );\n }\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Session model switching is not available on this server.');\n const threadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId,\n });\n if (!threadId) throw new Error('Session has no conversation thread yet.');\n // The thread's resource, not the session id: sessions share their Factory's or owner's resource.\n const resourceId = (await messaging.controller.queryThreadById({ threadId }))?.resourceId ?? sessionId;\n const catalog = await sessionModelCatalog(messaging.controller, sessionId, threadId, resourceId);\n if (!catalog.usable.includes(modelId)) {\n const options = catalog.usable.length\n ? ` Models available to it: ${catalog.usable.slice(0, LISTED_MODELS).join(', ')}${catalog.usable.length > LISTED_MODELS ? ', …' : ''}.`\n : ' No models are available to it; connect a provider credential first.';\n throw new Error(\n catalog.known.has(modelId)\n ? `Model ${modelId} exists, but this session's credential scope (${catalog.scopeLabel}) has no credential for its provider.${options}`\n : `Unknown model ${modelId}.${options}`,\n );\n }\n const { live } = await messaging.controller.setThreadModel({ resourceId, threadId, modelId });\n return { sessionId, threadId, modelId, live };\n },\n });\n\n const start = createTool({\n id: 'factory_start_session',\n description: [\n 'Start a side session in this Factory with a prompt. It runs on its own sandbox and branch as you, and reports back to this session with `factory-session` notifications.',\n 'Use it for self-contained investigations; when the result should land as a pull request, file a work item instead.',\n 'With `workItem` and `role` it starts that role on the work item instead, without moving the card, and this session follows the item.',\n \"A role that already has a session on the item (current or past) can't be started again from here: message that session with factory_signal_session, or ask the person to use the board.\",\n ].join(' '),\n inputSchema: z\n .object({\n prompt: z.string().trim().min(1).max(8000).describe('The first message the new session receives.'),\n title: z.string().trim().min(1).max(TITLE_LENGTH).optional(),\n workItem: workItemRefSchema.optional(),\n role: z.string().trim().min(1).max(64).optional(),\n })\n .strict()\n .refine(input => input.role === undefined || input.workItem !== undefined, {\n message: 'Pass workItem together with role.',\n path: ['role'],\n }),\n execute: async ({ prompt, title, workItem, role }) => {\n if (workItem !== undefined) {\n // A role start records a run as the person, so it waits for a turn the person started.\n if (!scope.interactive) {\n throw new Error(\n 'Starting a role on a work item needs a person in this turn; start a side session, or ask the person to use the board.',\n );\n }\n return startWorkItemRole({ prompt, title, workItem, role });\n }\n\n const parent = await deps.sessions.getBySessionId(scope.sessionId);\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!parent || !sourceControl) throw new Error('This session has no Factory session row to start from.');\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Starting sessions is not available on this server.');\n\n const sessionId = globalThis.crypto.randomUUID();\n const ensured = await ensureFactorySourceSession({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,\n // Bills the caller: the credential tenant follows the row's user.\n attributeToUserId: scope.userId,\n // Same repository as the session that started it.\n ...(parent.projectRepositoryId ? { projectRepositoryId: parent.projectRepositoryId } : {}),\n visibility: parent.visibility,\n origin: { sessionId: scope.sessionId, threadId: scope.threadId },\n });\n const sessionModel = await resolveFactorySessionModel({\n ...deps.sessionModel,\n factoryProjectId: scope.factoryProjectId,\n orgId: scope.orgId,\n ownerUserId: scope.userId,\n // No work item, so an interactive session: a hybrid Factory bills it as one.\n context: 'session',\n });\n\n // Only the new row's owner as `user`. The tool call's context carries the\n // interactive marker and the parent's controller entry; neither may reach the child.\n const kickoffContext = new RequestContext();\n kickoffContext.set('user', { workosId: scope.userId, organizationId: scope.orgId });\n\n const tags = { factoryProjectId: scope.factoryProjectId, projectRepositoryId: ensured.projectRepositoryId };\n const session = await messaging.controller.createSession({\n id: ensured.sessionId,\n ownerId: scope.userId,\n // A side session works for the Factory, so it shares the Factory's memory\n // resource; a private one stays with its owner, like a Slack DM.\n resourceId: parent.visibility === 'private' ? scope.userId : scope.factoryProjectId,\n threadId: ensured.sessionId,\n requestContext: kickoffContext,\n tags,\n });\n await session.state.set(tags);\n await hydrateFactorySession(session, {\n orgId: scope.orgId,\n modelScope: sessionModel.modelScope,\n ownerUserId: scope.userId,\n defaultModelId: sessionModel.modelId,\n });\n\n const threadId = session.thread.requireId();\n const threadTitle = title ?? titleFrom(prompt);\n await session.thread.rename({ title: threadTitle, pin: false });\n // On the row too, so its run-end reports name it without waiting for the title mirror.\n await sourceControl.sessions.rename({ sessionId: ensured.sessionId, title: threadTitle });\n await session.thread.setSetting({ key: 'factorySessionId', value: ensured.sessionId });\n\n await primeTenantCredentialsForTenant({\n orgId: scope.orgId,\n userId: scope.userId,\n credentialScope: sessionModel.modelScope,\n });\n // How its runs end reaches this session through the listener every Factory\n // session gets (`observeSessionRunReports`), which follows the row's origin.\n // `requireDelivery` makes `accepted` reject when the wake fails; the run itself is not awaited.\n await session.sendSignal(\n { id: globalThis.crypto.randomUUID(), type: 'user', tagName: 'user', contents: prompt },\n { requestContext: kickoffContext, requireDelivery: true },\n ).accepted;\n\n return { mode: 'side' as const, sessionId: ensured.sessionId, threadId, title: threadTitle };\n },\n });\n\n async function startWorkItemRole({\n prompt,\n title,\n workItem,\n role,\n }: {\n prompt: string;\n title?: string;\n workItem: string;\n role?: string;\n }) {\n if (!role) throw new Error('Pass the role to start on the work item.');\n const coordinator = deps.startCoordinator();\n if (!coordinator) throw new Error(\"Factory sessions can't be started yet; try again shortly.\");\n const item = await findWorkItem(deps.workItems, scope, workItem);\n if (!item) throw workItemNotFound(workItem);\n const label = workItemLabel(item);\n const board = deps.boards.get(boardForWorkItem(item));\n if (!board?.phaseForRole(role)) throw new Error(`${label}'s board has no ${role} role.`);\n const seated = item.sessions[role];\n if (seated) {\n throw new Error(\n `${label} already has a ${role} session ${seated.sessionId}; message it with factory_signal_session.`,\n );\n }\n\n const parent = await deps.sessions.getBySessionId(scope.sessionId);\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!parent || !sourceControl) throw new Error('This session has no Factory session row to start from.');\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Starting sessions is not available on this server.');\n const project = await deps.sessionModel.projects.get({ orgId: scope.orgId, id: scope.factoryProjectId });\n if (!project) throw new Error('This Factory no longer exists.');\n // A work-item start is a run, so a hybrid Factory uses the Factory's scope (as the Start route does).\n const modelScope = effectiveModelScope(project.modelScope, 'run');\n const projectRepositoryId = await resolveRepository(sourceControl, item);\n\n const sessionId = globalThis.crypto.randomUUID();\n const ensured = await ensureFactorySourceSession({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,\n projectRepositoryId,\n attributeToUserId: scope.userId,\n visibility: parent.visibility,\n });\n\n // Follow first, so a fast run's first report already has somewhere to go.\n const followScope = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n const alreadyFollowing = (\n await deps.workItems.listSessionSubscriptions({ ...followScope, sessionId: scope.sessionId })\n ).some(subscription => subscription.workItemId === item.id);\n await deps.workItems.subscribeToWorkItem({\n ...followScope,\n workItemId: item.id,\n sessionId: scope.sessionId,\n threadId: scope.threadId,\n resourceId: scope.resourceId,\n userId: scope.userId,\n reason: 'started',\n });\n\n const startedAt = new Date();\n let prepared;\n try {\n prepared = await coordinator.prepare({\n orgId: scope.orgId,\n userId: scope.userId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: ensured.sessionId,\n threadTitle: title ?? `${label} ${role}`,\n kickoffKey: globalThis.crypto.randomUUID(),\n modelScope,\n defaultModelId: modelScope === 'factory' ? (project.defaultModelId ?? undefined) : undefined,\n workItem: {\n id: item.id,\n role,\n input: {\n externalSource: item.externalSource,\n parentWorkItemId: item.parentWorkItemId,\n title: item.title,\n stages: item.stages,\n sessions: item.sessions,\n metadata: item.metadata,\n },\n },\n });\n } catch (error) {\n if (!alreadyFollowing) {\n await deps.workItems.unsubscribeFromWorkItem({\n orgId: scope.orgId,\n workItemId: item.id,\n sessionId: scope.sessionId,\n });\n }\n const message = startErrorMessage(error);\n if (message) throw new Error(message);\n throw error;\n }\n\n const bindings = await deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id);\n const replaced = bindings.find(\n binding =>\n binding.role === role &&\n binding.id !== prepared.bindingId &&\n binding.revokedAt !== null &&\n binding.revokedAt >= startedAt,\n );\n\n const session = await messaging.controller.getSession(prepared.sessionId);\n if (!session) throw new Error(`The ${role} session was bound but is not running; open it from the board.`);\n const kickoffId = globalThis.crypto.randomUUID();\n // Heartbeat from before the kickoff so a fast run's end is seen; the open-run\n // entry and audit follow the accepted kickoff, as the dispatcher records them.\n // The run's end reaches this item's followers through the session's listener.\n const heartbeat = heartbeatOpenRunUntilEnd(session, kickoffId);\n const kickoffContext = new RequestContext();\n kickoffContext.set('user', { workosId: scope.userId, organizationId: scope.orgId });\n try {\n await session.sendSignal(\n { id: kickoffId, type: 'user', tagName: 'user', contents: prompt },\n { requestContext: kickoffContext, requireDelivery: true },\n ).accepted;\n } catch (error) {\n heartbeat.stop();\n throw error;\n }\n\n await recordSessionRunStart(session, {\n audit: deps.auditRecorder,\n actorType: 'human',\n observedEnd: heartbeat.endReason,\n run: {\n kickoffId,\n ownerId: FACTORY_TOOLS_OWNER_ID,\n heartbeatAt: Date.now(),\n bindingId: prepared.bindingId,\n role,\n startedBy: scope.userId,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n workItemId: item.id,\n workItemName: item.title,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n branch: prepared.branch,\n },\n });\n\n const number = workItemNumber(item);\n return {\n mode: 'work-item' as const,\n workItem: { id: item.id, ...(number === null ? {} : { number }) },\n role,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n bindingId: prepared.bindingId,\n ...(replaced ? { replacedSessionId: replaced.sessionId } : {}),\n };\n }\n\n async function resolveRepository(sourceControl: SourceControlStorageHandle, item: WorkItemRow): Promise<string> {\n const intakeConfig = await deps.intake?.getConfig({ orgId: scope.orgId, integrationIds: ['linear'] });\n const repository = await resolveWorkItemRepository({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n item,\n linearRepositoryMap: intakeConfig?.linear?.repositoryByLinearProject,\n });\n if (repository.status === 'ambiguous') {\n throw new Error(ambiguousRepository(repository.candidates));\n }\n if (repository.status === 'unlinked') throw new Error(repository.hint);\n return repository.projectRepositoryId;\n }\n\n return { factory_start_session: start, factory_switch_session_model: switchModel };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"session-tools.js","names":[],"sources":["../../src/tools/session-tools.ts"],"sourcesContent":["import { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/registry.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { buildModelCatalog, listSessionScopedCredentials } from '../routes/config.js';\nimport { USER_SESSION_BRANCH_PREFIX } from '../routes/source-control-sessions.js';\nimport { primeTenantCredentialsForTenant } from '../routes/tenant-credentials.js';\nimport { FactoryStartTransitionError, WorkItemRepositoryError } from '../rules/start-coordinator.js';\nimport type { FactoryStartCoordinator } from '../rules/start-coordinator.js';\nimport { resolveFactoryModelPolicy } from '../session/credential-policy.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport {\n ensureFactorySourceSession,\n hydrateFactorySession,\n resolveFactorySessionModel,\n} from '../session/factory-session.js';\nimport { recordSessionRunStart } from '../session/run-audit.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { ModelCredentialsStorage } from '../storage/domains/credentials/base.js';\nimport type { CustomProvidersStorage } from '../storage/domains/custom-providers/base.js';\nimport type { IntakeStorage } from '../storage/domains/intake/base.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { deliverFactorySessionNotification, resolveSessionThread } from './deliver.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport { findWorkItem, workItemNotFound, workItemNumber, workItemRefSchema } from './read-tools.js';\nimport { heartbeatOpenRunUntilEnd } from './run-watch.js';\nimport type { FactoryToolScope } from './scope.js';\nimport { sessionModel } from './side-sessions.js';\n\nconst TITLE_LENGTH = 120;\n\n/** Who renews the open-run entries this process writes; one id per process, like the dispatcher's. */\nexport const FACTORY_TOOLS_OWNER_ID = `factory-tools:${globalThis.crypto.randomUUID()}`;\n\nexport interface FactorySessionToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'getSourceControlBySessionId'>;\n sessionModel: {\n projects: FactoryProjectsStorage;\n modelDefaults: ModelDefaultsStorage;\n /**\n * Tenant credential stores, present when auth is on. Model switches then\n * check the target session's credential scope, like the Settings picker;\n * without them the server's own keys decide.\n */\n tenantCatalog?: {\n credentials: Pick<ModelCredentialsStorage, 'listCredentials'>;\n customProviders?: Pick<CustomProvidersStorage, 'ensureReady' | 'list'>;\n deploymentProviders?: ReadonlySet<string>;\n };\n };\n /** Resolved lazily: the controller only exists once the mount is up. */\n messaging: () => Promise<FactoryMessaging | null>;\n workItems: Pick<\n WorkItemsStorage,\n | 'list'\n | 'getForProject'\n | 'listRunBindings'\n | 'findActiveRunBindingForSession'\n | 'findRunBindingByThread'\n | 'hasRunBindingForSession'\n | 'subscribeToWorkItem'\n | 'unsubscribeFromWorkItem'\n | 'listSessionSubscriptions'\n | 'listWorkItemSubscribers'\n >;\n boards: BoardRegistry;\n auditRecorder: AuditRecorder;\n intake?: Pick<IntakeStorage, 'getConfig'>;\n /** The browser Start's coordinator; absent until the Factory runtime is ready. */\n startCoordinator: () => Pick<FactoryStartCoordinator, 'prepare'> | undefined;\n}\n\nconst LISTED_MODELS = 8;\n\nfunction ambiguousRepository(candidates: readonly string[]): string {\n return `This item has several repositories (${candidates.join(', ')}); v1 can't choose one.`;\n}\n\nfunction startErrorMessage(error: unknown): string | null {\n if (error instanceof WorkItemRepositoryError) {\n return error.code === 'repository_ambiguous' ? ambiguousRepository(error.candidates) : error.message;\n }\n if (error instanceof FactoryStartTransitionError) return `The start was refused: ${error.result.reason}`;\n return null;\n}\n\nfunction workItemLabel(item: WorkItemRow): string {\n const number = workItemNumber(item);\n return number === null ? `\"${item.title}\"` : `#${number}`;\n}\n\nfunction titleFrom(prompt: string): string {\n const firstLine = prompt.split('\\n').find(line => line.trim()) ?? prompt;\n return firstLine.trim().slice(0, TITLE_LENGTH);\n}\n\n/** Tools that start sessions on the caller's behalf, or switch their models. User sessions only. */\nexport function createFactorySessionTools(\n scope: FactoryToolScope,\n deps: FactorySessionToolDependencies,\n): IntegrationTools {\n /** The models a session can switch to, resolved from the target session's owner and project policy. */\n async function sessionModelCatalog(\n controller: FactoryMessaging['controller'],\n sessionId: string,\n threadId: string,\n resourceId: string,\n ): Promise<{ usable: string[]; known: Set<string>; scopeLabel: string }> {\n const models = await controller.listAvailableModels();\n const known = new Set(models.flatMap(model => (typeof model.id === 'string' ? [model.id] : [])));\n const tenantCatalog = deps.sessionModel.tenantCatalog;\n if (!tenantCatalog) {\n const usable = (await buildModelCatalog({ controller })).map(model => model.id);\n return { usable, known, scopeLabel: 'server' };\n }\n const requestContext = new RequestContext();\n requestContext.set('user', { id: scope.userId, organizationId: scope.orgId });\n requestContext.set('controller', { sessionId, resourceId, threadId });\n const policy = await resolveFactoryModelPolicy(requestContext, {\n projects: deps.sessionModel.projects,\n sourceControl: deps.sessions,\n workItems: deps.workItems,\n });\n if (!policy) throw new Error('Session model policy is unavailable.');\n const modelScope = policy.tenant.credentialScope ?? 'user';\n const orgId = policy.tenant.orgId ?? scope.orgId;\n const customProviders = tenantCatalog.customProviders;\n const usable = (\n await buildModelCatalog({\n controller,\n tenantCredentials: await listSessionScopedCredentials({\n credentials: tenantCatalog.credentials,\n orgId,\n userId: policy.tenant.userId,\n modelScope,\n }),\n deploymentProviders: tenantCatalog.deploymentProviders,\n listCustomProviders: customProviders\n ? async () => {\n await customProviders.ensureReady();\n return customProviders.list({ orgId });\n }\n : undefined,\n })\n ).map(model => model.id);\n for (const id of usable) known.add(id);\n return { usable, known, scopeLabel: modelScope === 'factory' ? 'Factory' : 'session owner' };\n }\n\n function unrunnableModelError(\n modelId: string,\n catalog: { usable: string[]; known: Set<string>; scopeLabel: string },\n ) {\n const options = catalog.usable.length\n ? ` Models available to it: ${catalog.usable.slice(0, LISTED_MODELS).join(', ')}${catalog.usable.length > LISTED_MODELS ? ', …' : ''}.`\n : ' No models are available to it; connect a provider credential first.';\n return new Error(\n catalog.known.has(modelId)\n ? `Model ${modelId} exists, but this session's credential scope (${catalog.scopeLabel}) has no credential for its provider.${options}`\n : `Unknown model ${modelId}.${options}`,\n );\n }\n\n const switchModel = createTool({\n id: 'factory_switch_session_model',\n description:\n 'Switch the model of a session you own in this Factory (side, work item, or review session) without recreating it. A running session switches immediately; a stopped one uses the model on its next run. The model must be one the session credentials can run: the owner credentials for a user-scoped session, the Factory credentials for a Factory-scoped one. Use a session ID from the Factory session or work item tools.',\n inputSchema: z\n .object({\n sessionId: z.string().trim().min(1).max(200),\n modelId: z.string().trim().min(1).max(200),\n })\n .strict(),\n execute: async ({ sessionId, modelId }) => {\n const row = await deps.sessions.getBySessionId(sessionId);\n if (\n !row ||\n row.orgId !== scope.orgId ||\n row.factoryProjectId !== scope.factoryProjectId ||\n row.userId !== scope.userId\n ) {\n throw new Error('Session not found or not owned by you in this Factory.');\n }\n // A turn nobody signed in started reaches only this session and the side sessions it started.\n if (!scope.interactive && sessionId !== scope.sessionId && row.originSessionId !== scope.sessionId) {\n throw new Error(\n 'Without a person in this turn, only this session and the side sessions it started can switch models.',\n );\n }\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Session model switching is not available on this server.');\n const threadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId,\n });\n if (!threadId) throw new Error('Session has no conversation thread yet.');\n // The thread's resource, not the session id: sessions share their Factory's or owner's resource.\n const resourceId = (await messaging.controller.queryThreadById({ threadId }))?.resourceId ?? sessionId;\n const catalog = await sessionModelCatalog(messaging.controller, sessionId, threadId, resourceId);\n if (!catalog.usable.includes(modelId)) throw unrunnableModelError(modelId, catalog);\n const { live } = await messaging.controller.setThreadModel({ resourceId, threadId, modelId });\n return { sessionId, threadId, modelId, live };\n },\n });\n\n const start = createTool({\n id: 'factory_start_session',\n description: [\n 'Start a side session in this Factory with a prompt. It runs on its own sandbox and branch as you, and reports back to this session with `factory-session` notifications.',\n 'A side session uses this session’s model. Pass `model` to start it on another model the session credentials can run.',\n 'Use it for self-contained investigations; when the result should land as a pull request, file a work item instead.',\n 'With `workItem` and `role` it starts that role on the work item instead, without moving the card, and this session follows the item.',\n \"A role that already has a session on the item (current or past) can't be started again from here: message that session with factory_signal_session, or ask the person to use the board.\",\n ].join(' '),\n inputSchema: z\n .object({\n prompt: z.string().trim().min(1).max(8000).describe('The first message the new session receives.'),\n title: z.string().trim().min(1).max(TITLE_LENGTH).optional(),\n model: z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe(\n 'Model id for a side session. Omit it to inherit this session’s model. The session credentials must be able to run it.',\n ),\n workItem: workItemRefSchema.optional(),\n role: z.string().trim().min(1).max(64).optional(),\n })\n .strict()\n .refine(input => input.role === undefined || input.workItem !== undefined, {\n message: 'Pass workItem together with role.',\n path: ['role'],\n }),\n execute: async ({ prompt, title, model, workItem, role }) => {\n if (workItem !== undefined) {\n // A role start records a run as the person, so it waits for a turn the person started.\n if (!scope.interactive) {\n throw new Error(\n 'Starting a role on a work item needs a person in this turn; start a side session, or ask the person to use the board.',\n );\n }\n return startWorkItemRole({ prompt, title, workItem, role });\n }\n\n const parent = await deps.sessions.getBySessionId(scope.sessionId);\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!parent || !sourceControl) throw new Error('This session has no Factory session row to start from.');\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Starting sessions is not available on this server.');\n\n // The child runs with the same credentials as this session, so validate against those.\n const parentThreadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId: scope.sessionId,\n });\n const parentResourceId = parentThreadId\n ? ((await messaging.controller.queryThreadById({ threadId: parentThreadId }))?.resourceId ?? scope.sessionId)\n : scope.sessionId;\n const catalog = await sessionModelCatalog(\n messaging.controller,\n scope.sessionId,\n parentThreadId ?? scope.threadId,\n parentResourceId,\n );\n const inherited = model\n ? null\n : await sessionModel(messaging.controller, deps.sessions, deps.workItems, scope.sessionId);\n const requestedModel = model ?? (inherited && catalog.usable.includes(inherited) ? inherited : undefined);\n if (requestedModel && !catalog.usable.includes(requestedModel))\n throw unrunnableModelError(requestedModel, catalog);\n\n const sessionId = globalThis.crypto.randomUUID();\n const ensured = await ensureFactorySourceSession({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,\n // Bills the caller: the credential tenant follows the row's user.\n attributeToUserId: scope.userId,\n // Same repository as the session that started it.\n ...(parent.projectRepositoryId ? { projectRepositoryId: parent.projectRepositoryId } : {}),\n visibility: parent.visibility,\n origin: { sessionId: scope.sessionId, threadId: scope.threadId },\n });\n const resolvedModel = await resolveFactorySessionModel({\n ...deps.sessionModel,\n factoryProjectId: scope.factoryProjectId,\n orgId: scope.orgId,\n ownerUserId: scope.userId,\n // No work item, so an interactive session: a hybrid Factory bills it as one.\n context: 'session',\n });\n\n // Only the new row's owner as `user`. The tool call's context carries the\n // interactive marker and the parent's controller entry; neither may reach the child.\n const kickoffContext = new RequestContext();\n kickoffContext.set('user', { workosId: scope.userId, organizationId: scope.orgId });\n\n const tags = { factoryProjectId: scope.factoryProjectId, projectRepositoryId: ensured.projectRepositoryId };\n const session = await messaging.controller.createSession({\n id: ensured.sessionId,\n ownerId: scope.userId,\n // A side session works for the Factory, so it shares the Factory's memory\n // resource; a private one stays with its owner, like a Slack DM.\n resourceId: parent.visibility === 'private' ? scope.userId : scope.factoryProjectId,\n threadId: ensured.sessionId,\n requestContext: kickoffContext,\n tags,\n });\n await session.state.set(tags);\n await hydrateFactorySession(session, {\n orgId: scope.orgId,\n modelScope: resolvedModel.modelScope,\n ownerUserId: scope.userId,\n defaultModelId: requestedModel ?? resolvedModel.modelId,\n });\n\n const threadId = session.thread.requireId();\n const threadTitle = title ?? titleFrom(prompt);\n await session.thread.rename({ title: threadTitle, pin: false });\n // On the row too, so its run-end reports name it without waiting for the title mirror.\n await sourceControl.sessions.rename({ sessionId: ensured.sessionId, title: threadTitle });\n await session.thread.setSetting({ key: 'factorySessionId', value: ensured.sessionId });\n\n await primeTenantCredentialsForTenant({\n orgId: scope.orgId,\n userId: scope.userId,\n credentialScope: resolvedModel.modelScope,\n });\n // How its runs end reaches this session through the listener every Factory\n // session gets (`observeSessionRunReports`), which follows the row's origin.\n // `requireDelivery` makes `accepted` reject when the wake fails; the run itself is not awaited.\n await session.sendSignal(\n { id: globalThis.crypto.randomUUID(), type: 'user', tagName: 'user', contents: prompt },\n { requestContext: kickoffContext, requireDelivery: true },\n ).accepted;\n\n return { mode: 'side' as const, sessionId: ensured.sessionId, threadId, title: threadTitle };\n },\n });\n\n async function startWorkItemRole({\n prompt,\n title,\n workItem,\n role,\n }: {\n prompt: string;\n title?: string;\n workItem: string;\n role?: string;\n }) {\n if (!role) throw new Error('Pass the role to start on the work item.');\n const coordinator = deps.startCoordinator();\n if (!coordinator) throw new Error(\"Factory sessions can't be started yet; try again shortly.\");\n const item = await findWorkItem(deps.workItems, scope, workItem);\n if (!item) throw workItemNotFound(workItem);\n const label = workItemLabel(item);\n const board = deps.boards.get(boardForWorkItem(item));\n if (!board?.phaseForRole(role)) throw new Error(`${label}'s board has no ${role} role.`);\n const seated = item.sessions[role];\n if (seated) {\n throw new Error(\n `${label} already has a ${role} session ${seated.sessionId}; message it with factory_signal_session.`,\n );\n }\n\n const parent = await deps.sessions.getBySessionId(scope.sessionId);\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!parent || !sourceControl) throw new Error('This session has no Factory session row to start from.');\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Starting sessions is not available on this server.');\n const project = await deps.sessionModel.projects.get({ orgId: scope.orgId, id: scope.factoryProjectId });\n if (!project) throw new Error('This Factory no longer exists.');\n // A work-item start is a run, so a hybrid Factory uses the Factory's scope (as the Start route does).\n const modelScope = effectiveModelScope(project.modelScope, 'run');\n const projectRepositoryId = await resolveRepository(sourceControl, item);\n\n const sessionId = globalThis.crypto.randomUUID();\n const ensured = await ensureFactorySourceSession({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,\n projectRepositoryId,\n attributeToUserId: scope.userId,\n visibility: parent.visibility,\n });\n\n // Follow first, so a fast run's first report already has somewhere to go.\n const followScope = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n const alreadyFollowing = (\n await deps.workItems.listSessionSubscriptions({ ...followScope, sessionId: scope.sessionId })\n ).some(subscription => subscription.workItemId === item.id);\n await deps.workItems.subscribeToWorkItem({\n ...followScope,\n workItemId: item.id,\n sessionId: scope.sessionId,\n threadId: scope.threadId,\n resourceId: scope.resourceId,\n userId: scope.userId,\n reason: 'started',\n });\n\n const startedAt = new Date();\n let prepared;\n try {\n prepared = await coordinator.prepare({\n orgId: scope.orgId,\n userId: scope.userId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: ensured.sessionId,\n threadTitle: title ?? `${label} ${role}`,\n kickoffKey: globalThis.crypto.randomUUID(),\n modelScope,\n defaultModelId: modelScope === 'factory' ? (project.defaultModelId ?? undefined) : undefined,\n workItem: {\n id: item.id,\n role,\n input: {\n externalSource: item.externalSource,\n parentWorkItemId: item.parentWorkItemId,\n title: item.title,\n stages: item.stages,\n sessions: item.sessions,\n metadata: item.metadata,\n },\n },\n });\n } catch (error) {\n if (!alreadyFollowing) {\n await deps.workItems.unsubscribeFromWorkItem({\n orgId: scope.orgId,\n workItemId: item.id,\n sessionId: scope.sessionId,\n });\n }\n const message = startErrorMessage(error);\n if (message) throw new Error(message);\n throw error;\n }\n\n const bindings = await deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id);\n const replaced = bindings.find(\n binding =>\n binding.role === role &&\n binding.id !== prepared.bindingId &&\n binding.revokedAt !== null &&\n binding.revokedAt >= startedAt,\n );\n\n const session = await messaging.controller.getSession(prepared.sessionId);\n if (!session) throw new Error(`The ${role} session was bound but is not running; open it from the board.`);\n const kickoffId = globalThis.crypto.randomUUID();\n // Heartbeat from before the kickoff so a fast run's end is seen; the open-run\n // entry and audit follow the accepted kickoff, as the dispatcher records them.\n // The run's end reaches this item's followers through the session's listener.\n const heartbeat = heartbeatOpenRunUntilEnd(session, kickoffId);\n const kickoffContext = new RequestContext();\n kickoffContext.set('user', { workosId: scope.userId, organizationId: scope.orgId });\n try {\n await session.sendSignal(\n { id: kickoffId, type: 'user', tagName: 'user', contents: prompt },\n { requestContext: kickoffContext, requireDelivery: true },\n ).accepted;\n } catch (error) {\n heartbeat.stop();\n throw error;\n }\n\n await recordSessionRunStart(session, {\n audit: deps.auditRecorder,\n actorType: 'human',\n observedEnd: heartbeat.endReason,\n run: {\n kickoffId,\n ownerId: FACTORY_TOOLS_OWNER_ID,\n heartbeatAt: Date.now(),\n bindingId: prepared.bindingId,\n role,\n startedBy: scope.userId,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n workItemId: item.id,\n workItemName: item.title,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n branch: prepared.branch,\n },\n });\n\n const number = workItemNumber(item);\n return {\n mode: 'work-item' as const,\n workItem: { id: item.id, ...(number === null ? {} : { number }) },\n role,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n bindingId: prepared.bindingId,\n ...(replaced ? { replacedSessionId: replaced.sessionId } : {}),\n };\n }\n\n async function resolveRepository(sourceControl: SourceControlStorageHandle, item: WorkItemRow): Promise<string> {\n const intakeConfig = await deps.intake?.getConfig({ orgId: scope.orgId, integrationIds: ['linear'] });\n const repository = await resolveWorkItemRepository({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n item,\n linearRepositoryMap: intakeConfig?.linear?.repositoryByLinearProject,\n });\n if (repository.status === 'ambiguous') {\n throw new Error(ambiguousRepository(repository.candidates));\n }\n if (repository.status === 'unlinked') throw new Error(repository.hint);\n return repository.projectRepositoryId;\n }\n\n return { factory_start_session: start, factory_switch_session_model: switchModel };\n}\n"],"mappings":"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"}
@@ -6,11 +6,11 @@ import { createHash } from "crypto";
6
6
  //#region src/tools/sessions-state-processor.ts
7
7
  const STATE_ID = "factory-sessions";
8
8
  /** Finished side sessions and items that reached a terminal stage drop out after this long. */
9
- const FACTORY_SESSIONS_MAX_AGE_MS = 1440 * 60 * 1e3;
9
+ const FACTORY_SESSIONS_MAX_AGE_MS = 864e5;
10
10
  /** Asks a long run to show it is still moving; a silent half hour reads the same as a stuck one. */
11
11
  const PROGRESS_ASK = "On long work, send a progress report after each plan step.";
12
12
  /** A running worker whose thread has not moved for this long is shown as quiet, so a stuck run stands out. */
13
- const FACTORY_SESSIONS_QUIET_MS = 600 * 1e3;
13
+ const FACTORY_SESSIONS_QUIET_MS = 6e5;
14
14
  /** Rows shown before the rest collapse into "+N more". */
15
15
  const FACTORY_SESSIONS_MAX_ROWS = 20;
16
16
  const EMPTY_CACHE_KEY = "empty";