@mastra/factory 0.24.0-alpha.7 → 0.24.0-alpha.8

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 (118) hide show
  1. package/dist/boards/review.d.ts.map +1 -1
  2. package/dist/boards/review.js +21 -1
  3. package/dist/boards/review.js.map +1 -1
  4. package/dist/boards/work-transition-policy.d.ts.map +1 -1
  5. package/dist/boards/work-transition-policy.js +2 -1
  6. package/dist/boards/work-transition-policy.js.map +1 -1
  7. package/dist/factory.d.ts.map +1 -1
  8. package/dist/factory.js +91 -13
  9. package/dist/factory.js.map +1 -1
  10. package/dist/integrations/github/rules.d.ts.map +1 -1
  11. package/dist/integrations/github/rules.js +26 -22
  12. package/dist/integrations/github/rules.js.map +1 -1
  13. package/dist/integrations/github/sandbox.d.ts +2 -3
  14. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  15. package/dist/integrations/github/sandbox.js +78 -7
  16. package/dist/integrations/github/sandbox.js.map +1 -1
  17. package/dist/integrations/gitlab/rules.d.ts.map +1 -1
  18. package/dist/integrations/gitlab/rules.js +28 -21
  19. package/dist/integrations/gitlab/rules.js.map +1 -1
  20. package/dist/integrations/slack/integration.d.ts.map +1 -1
  21. package/dist/integrations/slack/integration.js +33 -6
  22. package/dist/integrations/slack/integration.js.map +1 -1
  23. package/dist/integrations/slack/slack.d.ts.map +1 -1
  24. package/dist/integrations/slack/slack.js +25 -24
  25. package/dist/integrations/slack/slack.js.map +1 -1
  26. package/dist/routes/config.d.ts +4 -0
  27. package/dist/routes/config.d.ts.map +1 -1
  28. package/dist/routes/config.js +20 -6
  29. package/dist/routes/config.js.map +1 -1
  30. package/dist/routes/provider-credentials.d.ts +2 -1
  31. package/dist/routes/provider-credentials.d.ts.map +1 -1
  32. package/dist/routes/provider-credentials.js +6 -2
  33. package/dist/routes/provider-credentials.js.map +1 -1
  34. package/dist/rules/dispatch-errors.d.ts.map +1 -1
  35. package/dist/rules/dispatch-errors.js +12 -0
  36. package/dist/rules/dispatch-errors.js.map +1 -1
  37. package/dist/rules/dispatcher.d.ts.map +1 -1
  38. package/dist/rules/dispatcher.js +42 -3
  39. package/dist/rules/dispatcher.js.map +1 -1
  40. package/dist/session/channel-reply-processor.d.ts +35 -0
  41. package/dist/session/channel-reply-processor.d.ts.map +1 -0
  42. package/dist/session/channel-reply-processor.js +46 -0
  43. package/dist/session/channel-reply-processor.js.map +1 -0
  44. package/dist/session/channel-session-link-processor.d.ts +39 -0
  45. package/dist/session/channel-session-link-processor.d.ts.map +1 -0
  46. package/dist/session/channel-session-link-processor.js +99 -0
  47. package/dist/session/channel-session-link-processor.js.map +1 -0
  48. package/dist/session/factory-session.d.ts +4 -0
  49. package/dist/session/factory-session.d.ts.map +1 -1
  50. package/dist/session/factory-session.js +1 -0
  51. package/dist/session/factory-session.js.map +1 -1
  52. package/dist/session/reply-credential-processor.d.ts.map +1 -1
  53. package/dist/session/reply-credential-processor.js +4 -6
  54. package/dist/session/reply-credential-processor.js.map +1 -1
  55. package/dist/session/run-audit.d.ts +24 -0
  56. package/dist/session/run-audit.d.ts.map +1 -1
  57. package/dist/session/run-audit.js +93 -7
  58. package/dist/session/run-audit.js.map +1 -1
  59. package/dist/session/source-control-tools.d.ts.map +1 -1
  60. package/dist/session/source-control-tools.js.map +1 -1
  61. package/dist/storage/domains/source-control/base.d.ts +7 -0
  62. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  63. package/dist/storage/domains/source-control/base.js +12 -0
  64. package/dist/storage/domains/source-control/base.js.map +1 -1
  65. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  66. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  67. package/dist/storage/domains/source-control/inmemory.js +8 -0
  68. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  69. package/dist/storage/domains/work-items/base.d.ts +2 -2
  70. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  71. package/dist/storage/domains/work-items/base.js +5 -0
  72. package/dist/storage/domains/work-items/base.js.map +1 -1
  73. package/dist/tools/approval-tools.d.ts +1 -1
  74. package/dist/tools/approval-tools.d.ts.map +1 -1
  75. package/dist/tools/approval-tools.js +3 -1
  76. package/dist/tools/approval-tools.js.map +1 -1
  77. package/dist/tools/deliver.d.ts +16 -2
  78. package/dist/tools/deliver.d.ts.map +1 -1
  79. package/dist/tools/deliver.js +12 -2
  80. package/dist/tools/deliver.js.map +1 -1
  81. package/dist/tools/index.d.ts +6 -5
  82. package/dist/tools/index.d.ts.map +1 -1
  83. package/dist/tools/index.js +9 -12
  84. package/dist/tools/index.js.map +1 -1
  85. package/dist/tools/messaging-tools.d.ts +1 -1
  86. package/dist/tools/messaging-tools.d.ts.map +1 -1
  87. package/dist/tools/messaging-tools.js +84 -3
  88. package/dist/tools/messaging-tools.js.map +1 -1
  89. package/dist/tools/permissions.d.ts.map +1 -1
  90. package/dist/tools/permissions.js +3 -0
  91. package/dist/tools/permissions.js.map +1 -1
  92. package/dist/tools/read-tools.d.ts +3 -0
  93. package/dist/tools/read-tools.d.ts.map +1 -1
  94. package/dist/tools/read-tools.js +16 -8
  95. package/dist/tools/read-tools.js.map +1 -1
  96. package/dist/tools/reports.d.ts.map +1 -1
  97. package/dist/tools/reports.js +3 -1
  98. package/dist/tools/reports.js.map +1 -1
  99. package/dist/tools/scope.d.ts +6 -0
  100. package/dist/tools/scope.d.ts.map +1 -1
  101. package/dist/tools/scope.js +10 -1
  102. package/dist/tools/scope.js.map +1 -1
  103. package/dist/tools/session-tools.d.ts +1 -1
  104. package/dist/tools/session-tools.d.ts.map +1 -1
  105. package/dist/tools/session-tools.js +11 -7
  106. package/dist/tools/session-tools.js.map +1 -1
  107. package/dist/tools/sessions-state-processor.d.ts +5 -1
  108. package/dist/tools/sessions-state-processor.d.ts.map +1 -1
  109. package/dist/tools/sessions-state-processor.js +32 -48
  110. package/dist/tools/sessions-state-processor.js.map +1 -1
  111. package/dist/tools/side-sessions.d.ts +53 -0
  112. package/dist/tools/side-sessions.d.ts.map +1 -0
  113. package/dist/tools/side-sessions.js +83 -0
  114. package/dist/tools/side-sessions.js.map +1 -0
  115. package/dist/tools/work-item-tools.d.ts.map +1 -1
  116. package/dist/tools/work-item-tools.js +45 -25
  117. package/dist/tools/work-item-tools.js.map +1 -1
  118. package/package.json +6 -6
@@ -10,6 +10,6 @@ export interface FactoryApprovalToolDependencies {
10
10
  auditRecorder: AuditRecorder;
11
11
  messaging: () => Promise<FactoryMessaging | null>;
12
12
  }
13
- /** Answers a tool call another session holds for approval. Interactive user turns only. */
13
+ /** Answers a tool call another session holds for approval. Only a signed-in person's turn may answer. */
14
14
  export declare function createFactoryApprovalTools(scope: FactoryToolScope, deps: FactoryApprovalToolDependencies): IntegrationTools;
15
15
  //# sourceMappingURL=approval-tools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"approval-tools.d.ts","sourceRoot":"","sources":["../../src/tools/approval-tools.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;IAC7D,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,gCAAgC,GAAG,0BAA0B,CAAC,CAAC;IACjG,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CACnD;AAYD,2FAA2F;AAC3F,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,+BAA+B,GACpC,gBAAgB,CAmFlB"}
1
+ {"version":3,"file":"approval-tools.d.ts","sourceRoot":"","sources":["../../src/tools/approval-tools.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAE7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;IAC7D,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,gCAAgC,GAAG,0BAA0B,CAAC,CAAC;IACjG,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CACnD;AAYD,yGAAyG;AACzG,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,+BAA+B,GACpC,gBAAgB,CAoFlB"}
@@ -1,7 +1,8 @@
1
+ import { requirePerson } from "./scope.js";
1
2
  import { z } from "zod";
2
3
  import { createTool } from "@mastra/core/tools";
3
4
  //#region src/tools/approval-tools.ts
4
- /** Answers a tool call another session holds for approval. Interactive user turns only. */
5
+ /** Answers a tool call another session holds for approval. Only a signed-in person's turn may answer. */
5
6
  function createFactoryApprovalTools(scope, deps) {
6
7
  return { factory_answer_approval: createTool({
7
8
  id: "factory_answer_approval",
@@ -17,6 +18,7 @@ function createFactoryApprovalTools(scope, deps) {
17
18
  reason: z.string().trim().min(1).max(1e3).optional().describe("Why, when declining.")
18
19
  }).strict(),
19
20
  execute: async ({ sessionId, toolCallId, decision, reason }) => {
21
+ requirePerson(scope, "answer an approval");
20
22
  const row = await deps.sessions.getBySessionId(sessionId);
21
23
  if (!row || row.orgId !== scope.orgId || row.factoryProjectId !== scope.factoryProjectId) throw new Error(`Session ${sessionId} was not found in this Factory.`);
22
24
  if (row.visibility === "private" && row.userId !== scope.userId) throw new Error(`Session ${sessionId} is private to its owner.`);
@@ -1 +1 @@
1
- {"version":3,"file":"approval-tools.js","names":[],"sources":["../../src/tools/approval-tools.ts"],"sourcesContent":["import { 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 { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport type { FactoryToolScope } from './scope.js';\n\nexport interface FactoryApprovalToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession' | 'listSessionSubscriptions'>;\n auditRecorder: AuditRecorder;\n messaging: () => Promise<FactoryMessaging | null>;\n}\n\n/** The parts of a live session an answer needs: its open approval gates. */\ninterface ApprovalGateSession {\n approval: { isArmed(filter: { toolCallId: string }): boolean };\n respondToToolApproval(input: {\n decision: 'approve' | 'decline';\n toolCallId: string;\n declineContext?: { reason?: string };\n }): { accepted: true } | { accepted: false; reason: string };\n}\n\n/** Answers a tool call another session holds for approval. Interactive user turns only. */\nexport function createFactoryApprovalTools(\n scope: FactoryToolScope,\n deps: FactoryApprovalToolDependencies,\n): IntegrationTools {\n const answer = createTool({\n id: 'factory_answer_approval',\n description: [\n 'Approve or decline a tool call a session you follow is waiting on, as you.',\n 'A `factory-session` notification of kind `approval` names the session, tool, arguments and toolCallId.',\n 'Answer only what the person in this conversation agreed to; decline with a reason the session will see.',\n ].join(' '),\n inputSchema: z\n .object({\n sessionId: z.string().uuid().describe('The session holding the call, from the approval notice.'),\n toolCallId: z.string().trim().min(1).max(200),\n decision: z.enum(['approve', 'decline']),\n reason: z.string().trim().min(1).max(1000).optional().describe('Why, when declining.'),\n })\n .strict(),\n execute: async ({ sessionId, toolCallId, decision, reason }) => {\n const row = await deps.sessions.getBySessionId(sessionId);\n if (!row || row.orgId !== scope.orgId || row.factoryProjectId !== scope.factoryProjectId) {\n throw new Error(`Session ${sessionId} was not found in this Factory.`);\n }\n if (row.visibility === 'private' && row.userId !== scope.userId) {\n throw new Error(`Session ${sessionId} is private to its owner.`);\n }\n\n // Only the sessions it reports to may answer it: its origin, or followers of its item.\n const binding = await deps.workItems.findActiveRunBindingForSession({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n });\n const isOrigin = row.originSessionId === scope.sessionId;\n const follows =\n binding !== null &&\n (\n await deps.workItems.listSessionSubscriptions({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n })\n ).some(subscription => subscription.workItemId === binding.workItemId);\n if (!isOrigin && !follows) {\n throw new Error(\n `This session doesn't follow session ${sessionId}: follow its work item first, or answer from that session.`,\n );\n }\n\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Answering approvals is not available on this server.');\n const live = (await messaging.controller.getSessionByResource(sessionId)) as ApprovalGateSession | null;\n if (!live?.approval.isArmed({ toolCallId })) {\n throw new Error(\n 'Nothing is waiting on that tool call any more: the run that asked has ended or it was already answered. ' +\n 'Message the session with factory_signal_session if it should try again.',\n );\n }\n const result = live.respondToToolApproval({\n decision,\n toolCallId,\n ...(decision === 'decline'\n ? { declineContext: { reason: reason ?? 'Declined by a session following it.' } }\n : {}),\n });\n if (!result.accepted) throw new Error(`The answer was not accepted (${result.reason}).`);\n\n deps.auditRecorder.record({\n orgId: scope.orgId,\n actorId: scope.userId,\n actorType: 'human',\n ...(scope.actorProfile ? { actorProfile: scope.actorProfile } : {}),\n action: decision === 'approve' ? 'factory.run.approved' : 'factory.run.dismissed',\n factoryProjectId: scope.factoryProjectId,\n targets: [\n { type: 'session', id: sessionId },\n ...(binding ? [{ type: 'work_item' as const, id: binding.workItemId }] : []),\n ],\n metadata: { toolCallId, sessionId, decision, cause: 'factory_tools' },\n });\n return { answered: decision, sessionId, toolCallId };\n },\n });\n\n return { factory_answer_approval: answer };\n}\n"],"mappings":";;;;AA4BA,SAAgB,2BACd,OACA,MACkB;CAkFlB,OAAO,EAAE,yBAjFM,WAAW;EACxB,IAAI;EACJ,aAAa;GACX;GACA;GACA;EACF,CAAC,CAAC,KAAK,GAAG;EACV,aAAa,EACV,OAAO;GACN,WAAW,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,yDAAyD;GAC/F,YAAY,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG;GAC5C,UAAU,EAAE,KAAK,CAAC,WAAW,SAAS,CAAC;GACvC,QAAQ,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,sBAAsB;EACvF,CAAC,CAAC,CACD,OAAO;EACV,SAAS,OAAO,EAAE,WAAW,YAAY,UAAU,aAAa;GAC9D,MAAM,MAAM,MAAM,KAAK,SAAS,eAAe,SAAS;GACxD,IAAI,CAAC,OAAO,IAAI,UAAU,MAAM,SAAS,IAAI,qBAAqB,MAAM,kBACtE,MAAM,IAAI,MAAM,WAAW,UAAU,gCAAgC;GAEvE,IAAI,IAAI,eAAe,aAAa,IAAI,WAAW,MAAM,QACvD,MAAM,IAAI,MAAM,WAAW,UAAU,0BAA0B;GAIjE,MAAM,UAAU,MAAM,KAAK,UAAU,+BAA+B;IAClE,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB;GACF,CAAC;GACD,MAAM,WAAW,IAAI,oBAAoB,MAAM;GAC/C,MAAM,UACJ,YAAY,SAEV,MAAM,KAAK,UAAU,yBAAyB;IAC5C,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB,WAAW,MAAM;GACnB,CAAC,EAAA,CACD,MAAK,iBAAgB,aAAa,eAAe,QAAQ,UAAU;GACvE,IAAI,CAAC,YAAY,CAAC,SAChB,MAAM,IAAI,MACR,uCAAuC,UAAU,2DACnD;GAGF,MAAM,YAAY,MAAM,KAAK,UAAU;GACvC,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,sDAAsD;GACtF,MAAM,OAAQ,MAAM,UAAU,WAAW,qBAAqB,SAAS;GACvE,IAAI,CAAC,MAAM,SAAS,QAAQ,EAAE,WAAW,CAAC,GACxC,MAAM,IAAI,MACR,iLAEF;GAEF,MAAM,SAAS,KAAK,sBAAsB;IACxC;IACA;IACA,GAAI,aAAa,YACb,EAAE,gBAAgB,EAAE,QAAQ,UAAU,sCAAsC,EAAE,IAC9E,CAAC;GACP,CAAC;GACD,IAAI,CAAC,OAAO,UAAU,MAAM,IAAI,MAAM,gCAAgC,OAAO,OAAO,GAAG;GAEvF,KAAK,cAAc,OAAO;IACxB,OAAO,MAAM;IACb,SAAS,MAAM;IACf,WAAW;IACX,GAAI,MAAM,eAAe,EAAE,cAAc,MAAM,aAAa,IAAI,CAAC;IACjE,QAAQ,aAAa,YAAY,yBAAyB;IAC1D,kBAAkB,MAAM;IACxB,SAAS,CACP;KAAE,MAAM;KAAW,IAAI;IAAU,GACjC,GAAI,UAAU,CAAC;KAAE,MAAM;KAAsB,IAAI,QAAQ;IAAW,CAAC,IAAI,CAAC,CAC5E;IACA,UAAU;KAAE;KAAY;KAAW;KAAU,OAAO;IAAgB;GACtE,CAAC;GACD,OAAO;IAAE,UAAU;IAAU;IAAW;GAAW;EACrD;CACF,CAEuC,EAAE;AAC3C"}
1
+ {"version":3,"file":"approval-tools.js","names":[],"sources":["../../src/tools/approval-tools.ts"],"sourcesContent":["import { 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 { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport { requirePerson } from './scope.js';\nimport type { FactoryToolScope } from './scope.js';\n\nexport interface FactoryApprovalToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession' | 'listSessionSubscriptions'>;\n auditRecorder: AuditRecorder;\n messaging: () => Promise<FactoryMessaging | null>;\n}\n\n/** The parts of a live session an answer needs: its open approval gates. */\ninterface ApprovalGateSession {\n approval: { isArmed(filter: { toolCallId: string }): boolean };\n respondToToolApproval(input: {\n decision: 'approve' | 'decline';\n toolCallId: string;\n declineContext?: { reason?: string };\n }): { accepted: true } | { accepted: false; reason: string };\n}\n\n/** Answers a tool call another session holds for approval. Only a signed-in person's turn may answer. */\nexport function createFactoryApprovalTools(\n scope: FactoryToolScope,\n deps: FactoryApprovalToolDependencies,\n): IntegrationTools {\n const answer = createTool({\n id: 'factory_answer_approval',\n description: [\n 'Approve or decline a tool call a session you follow is waiting on, as you.',\n 'A `factory-session` notification of kind `approval` names the session, tool, arguments and toolCallId.',\n 'Answer only what the person in this conversation agreed to; decline with a reason the session will see.',\n ].join(' '),\n inputSchema: z\n .object({\n sessionId: z.string().uuid().describe('The session holding the call, from the approval notice.'),\n toolCallId: z.string().trim().min(1).max(200),\n decision: z.enum(['approve', 'decline']),\n reason: z.string().trim().min(1).max(1000).optional().describe('Why, when declining.'),\n })\n .strict(),\n execute: async ({ sessionId, toolCallId, decision, reason }) => {\n requirePerson(scope, 'answer an approval');\n const row = await deps.sessions.getBySessionId(sessionId);\n if (!row || row.orgId !== scope.orgId || row.factoryProjectId !== scope.factoryProjectId) {\n throw new Error(`Session ${sessionId} was not found in this Factory.`);\n }\n if (row.visibility === 'private' && row.userId !== scope.userId) {\n throw new Error(`Session ${sessionId} is private to its owner.`);\n }\n\n // Only the sessions it reports to may answer it: its origin, or followers of its item.\n const binding = await deps.workItems.findActiveRunBindingForSession({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n });\n const isOrigin = row.originSessionId === scope.sessionId;\n const follows =\n binding !== null &&\n (\n await deps.workItems.listSessionSubscriptions({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n })\n ).some(subscription => subscription.workItemId === binding.workItemId);\n if (!isOrigin && !follows) {\n throw new Error(\n `This session doesn't follow session ${sessionId}: follow its work item first, or answer from that session.`,\n );\n }\n\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Answering approvals is not available on this server.');\n const live = (await messaging.controller.getSessionByResource(sessionId)) as ApprovalGateSession | null;\n if (!live?.approval.isArmed({ toolCallId })) {\n throw new Error(\n 'Nothing is waiting on that tool call any more: the run that asked has ended or it was already answered. ' +\n 'Message the session with factory_signal_session if it should try again.',\n );\n }\n const result = live.respondToToolApproval({\n decision,\n toolCallId,\n ...(decision === 'decline'\n ? { declineContext: { reason: reason ?? 'Declined by a session following it.' } }\n : {}),\n });\n if (!result.accepted) throw new Error(`The answer was not accepted (${result.reason}).`);\n\n deps.auditRecorder.record({\n orgId: scope.orgId,\n actorId: scope.userId,\n actorType: 'human',\n ...(scope.actorProfile ? { actorProfile: scope.actorProfile } : {}),\n action: decision === 'approve' ? 'factory.run.approved' : 'factory.run.dismissed',\n factoryProjectId: scope.factoryProjectId,\n targets: [\n { type: 'session', id: sessionId },\n ...(binding ? [{ type: 'work_item' as const, id: binding.workItemId }] : []),\n ],\n metadata: { toolCallId, sessionId, decision, cause: 'factory_tools' },\n });\n return { answered: decision, sessionId, toolCallId };\n },\n });\n\n return { factory_answer_approval: answer };\n}\n"],"mappings":";;;;;AA6BA,SAAgB,2BACd,OACA,MACkB;CAmFlB,OAAO,EAAE,yBAlFM,WAAW;EACxB,IAAI;EACJ,aAAa;GACX;GACA;GACA;EACF,CAAC,CAAC,KAAK,GAAG;EACV,aAAa,EACV,OAAO;GACN,WAAW,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,yDAAyD;GAC/F,YAAY,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG;GAC5C,UAAU,EAAE,KAAK,CAAC,WAAW,SAAS,CAAC;GACvC,QAAQ,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAI,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,sBAAsB;EACvF,CAAC,CAAC,CACD,OAAO;EACV,SAAS,OAAO,EAAE,WAAW,YAAY,UAAU,aAAa;GAC9D,cAAc,OAAO,oBAAoB;GACzC,MAAM,MAAM,MAAM,KAAK,SAAS,eAAe,SAAS;GACxD,IAAI,CAAC,OAAO,IAAI,UAAU,MAAM,SAAS,IAAI,qBAAqB,MAAM,kBACtE,MAAM,IAAI,MAAM,WAAW,UAAU,gCAAgC;GAEvE,IAAI,IAAI,eAAe,aAAa,IAAI,WAAW,MAAM,QACvD,MAAM,IAAI,MAAM,WAAW,UAAU,0BAA0B;GAIjE,MAAM,UAAU,MAAM,KAAK,UAAU,+BAA+B;IAClE,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB;GACF,CAAC;GACD,MAAM,WAAW,IAAI,oBAAoB,MAAM;GAC/C,MAAM,UACJ,YAAY,SAEV,MAAM,KAAK,UAAU,yBAAyB;IAC5C,OAAO,MAAM;IACb,kBAAkB,MAAM;IACxB,WAAW,MAAM;GACnB,CAAC,EAAA,CACD,MAAK,iBAAgB,aAAa,eAAe,QAAQ,UAAU;GACvE,IAAI,CAAC,YAAY,CAAC,SAChB,MAAM,IAAI,MACR,uCAAuC,UAAU,2DACnD;GAGF,MAAM,YAAY,MAAM,KAAK,UAAU;GACvC,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,sDAAsD;GACtF,MAAM,OAAQ,MAAM,UAAU,WAAW,qBAAqB,SAAS;GACvE,IAAI,CAAC,MAAM,SAAS,QAAQ,EAAE,WAAW,CAAC,GACxC,MAAM,IAAI,MACR,iLAEF;GAEF,MAAM,SAAS,KAAK,sBAAsB;IACxC;IACA;IACA,GAAI,aAAa,YACb,EAAE,gBAAgB,EAAE,QAAQ,UAAU,sCAAsC,EAAE,IAC9E,CAAC;GACP,CAAC;GACD,IAAI,CAAC,OAAO,UAAU,MAAM,IAAI,MAAM,gCAAgC,OAAO,OAAO,GAAG;GAEvF,KAAK,cAAc,OAAO;IACxB,OAAO,MAAM;IACb,SAAS,MAAM;IACf,WAAW;IACX,GAAI,MAAM,eAAe,EAAE,cAAc,MAAM,aAAa,IAAI,CAAC;IACjE,QAAQ,aAAa,YAAY,yBAAyB;IAC1D,kBAAkB,MAAM;IACxB,SAAS,CACP;KAAE,MAAM;KAAW,IAAI;IAAU,GACjC,GAAI,UAAU,CAAC;KAAE,MAAM;KAAsB,IAAI,QAAQ;IAAW,CAAC,IAAI,CAAC,CAC5E;IACA,UAAU;KAAE;KAAY;KAAW;KAAU,OAAO;IAAgB;GACtE,CAAC;GACD,OAAO;IAAE,UAAU;IAAU;IAAW;GAAW;EACrD;CACF,CAEuC,EAAE;AAC3C"}
@@ -11,8 +11,14 @@ export interface FactoryDeliveryTarget {
11
11
  /** The stored thread the notification belongs to. Never the session's current thread by default. */
12
12
  threadId: string;
13
13
  }
14
- export type FactoryDeliveryResult = {
15
- delivered: 'woken' | 'persisted' | 'summarized';
14
+ export type FactoryDeliveryResult =
15
+ /**
16
+ * `woken`: the target was idle and the notification started a run.
17
+ * `mid-run`: the target was already running; it reads the notification when that run
18
+ * next checks its inbox, not now.
19
+ */
20
+ {
21
+ delivered: 'woken' | 'mid-run' | 'persisted' | 'summarized';
16
22
  notificationId: string;
17
23
  reason?: string;
18
24
  } | {
@@ -41,6 +47,14 @@ export declare function deliverFactorySessionNotification({ controller, sessions
41
47
  /** Skip, without creating anything, when no live session owns the target (stage notices). */
42
48
  onlyIfLive?: boolean;
43
49
  }): Promise<FactoryDeliveryResult>;
50
+ export interface FactorySessionActivity {
51
+ /** Whether the session's agent is mid-run in this process. */
52
+ running: boolean;
53
+ /** When the session's thread last changed (a message saved, a run step persisted), ISO. */
54
+ lastActivityAt: string | null;
55
+ }
56
+ /** Whether a session is busy right now, and when its thread last moved, so a stuck run is told apart from a long one. */
57
+ export declare function sessionActivity(controller: Pick<FactoryDeliveryController, 'getSessionByResource' | 'queryThreadById'>, sessionId: string, threadId: string): Promise<FactorySessionActivity>;
44
58
  /**
45
59
  * The thread a message to a session should land on: the thread its role runs
46
60
  * on, else the thread it has open, else its most recently updated thread.
@@ -1 +1 @@
1
- {"version":3,"file":"deliver.d.ts","sourceRoot":"","sources":["../../src/tools/deliver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAG9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,oEAAoE;AACpE,eAAO,MAAM,mCAAmC,oBAAoB,CAAC;AAErE,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAC1C,iBAAiB,CAAC,YAAY,CAAC,EAC7B,iBAAiB,GACjB,cAAc,GACd,gBAAgB,GAChB,qBAAqB,GACrB,sBAAsB,GACtB,eAAe,GACf,iBAAiB,CACpB,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,SAAS,EAAE,OAAO,GAAG,WAAW,GAAG,YAAY,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC5F;IACE,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,GAAG,mBAAmB,GAAG,SAAS,GAAG,WAAW,CAAC;CACjG,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAsB,iCAAiC,CAAC,EACtD,UAAU,EACV,QAAQ,EACR,MAAM,EACN,YAAY,EACZ,UAAkB,GACnB,EAAE;IACD,UAAU,EAAE,yBAAyB,CAAC;IACtC,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;IAC7D,MAAM,EAAE,qBAAqB,CAAC;IAC9B,YAAY,EAAE,IAAI,CAAC,2BAA2B,EAAE,QAAQ,CAAC,CAAC;IAC1D,6FAA6F;IAC7F,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAiDjC;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,UAAU,EACV,QAAQ,EACR,SAAS,EACT,SAAS,GACV,EAAE;IACD,UAAU,EAAE,IAAI,CAAC,yBAAyB,EAAE,sBAAsB,GAAG,cAAc,CAAC,CAAC;IACrF,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;IAC7D,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,gCAAgC,CAAC,CAAC;IACpE,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgBzB"}
1
+ {"version":3,"file":"deliver.d.ts","sourceRoot":"","sources":["../../src/tools/deliver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAG9E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,oEAAoE;AACpE,eAAO,MAAM,mCAAmC,oBAAoB,CAAC;AAErE,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAC1C,iBAAiB,CAAC,YAAY,CAAC,EAC7B,iBAAiB,GACjB,cAAc,GACd,gBAAgB,GAChB,qBAAqB,GACrB,sBAAsB,GACtB,eAAe,GACf,iBAAiB,CACpB,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,MAAM,qBAAqB;AAC/B;;;;GAIG;AACD;IAAE,SAAS,EAAE,OAAO,GAAG,SAAS,GAAG,WAAW,GAAG,YAAY,CAAC;IAAC,cAAc,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GACxG;IACE,SAAS,EAAE,SAAS,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,GAAG,mBAAmB,GAAG,SAAS,GAAG,WAAW,CAAC;CACjG,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAsB,iCAAiC,CAAC,EACtD,UAAU,EACV,QAAQ,EACR,MAAM,EACN,YAAY,EACZ,UAAkB,GACnB,EAAE;IACD,UAAU,EAAE,yBAAyB,CAAC;IACtC,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;IAC7D,MAAM,EAAE,qBAAqB,CAAC;IAC9B,YAAY,EAAE,IAAI,CAAC,2BAA2B,EAAE,QAAQ,CAAC,CAAC;IAC1D,6FAA6F;IAC7F,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAoDjC;AAED,MAAM,WAAW,sBAAsB;IACrC,8DAA8D;IAC9D,OAAO,EAAE,OAAO,CAAC;IACjB,2FAA2F;IAC3F,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,yHAAyH;AACzH,wBAAsB,eAAe,CACnC,UAAU,EAAE,IAAI,CAAC,yBAAyB,EAAE,sBAAsB,GAAG,iBAAiB,CAAC,EACvF,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,sBAAsB,CAAC,CAUjC;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EACzC,UAAU,EACV,QAAQ,EACR,SAAS,EACT,SAAS,GACV,EAAE;IACD,UAAU,EAAE,IAAI,CAAC,yBAAyB,EAAE,sBAAsB,GAAG,cAAc,CAAC,CAAC;IACrF,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;IAC7D,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,gCAAgC,CAAC,CAAC;IACpE,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAgBzB"}
@@ -48,6 +48,7 @@ async function deliverFactorySessionNotification({ controller, sessions, target,
48
48
  threadId: target.threadId
49
49
  });
50
50
  }
51
+ const busy = session.stream.isActive() && session.thread.getId() === target.threadId;
51
52
  const result = await controller.getCurrentAgent(session).sendNotificationSignal({
52
53
  ...notification,
53
54
  source: FACTORY_SESSION_NOTIFICATION_SOURCE
@@ -60,7 +61,7 @@ async function deliverFactorySessionNotification({ controller, sessions, target,
60
61
  const reason = result.decision.reason ? { reason: result.decision.reason } : {};
61
62
  switch (result.decision.action) {
62
63
  case "deliver": return {
63
- delivered: "woken",
64
+ delivered: busy ? "mid-run" : "woken",
64
65
  notificationId,
65
66
  ...reason
66
67
  };
@@ -81,6 +82,15 @@ async function deliverFactorySessionNotification({ controller, sessions, target,
81
82
  };
82
83
  }
83
84
  }
85
+ /** Whether a session is busy right now, and when its thread last moved, so a stuck run is told apart from a long one. */
86
+ async function sessionActivity(controller, sessionId, threadId) {
87
+ const [live, thread] = await Promise.all([controller.getSessionByResource(sessionId), controller.queryThreadById({ threadId })]);
88
+ const updatedAt = thread?.updatedAt ? new Date(thread.updatedAt) : null;
89
+ return {
90
+ running: live?.stream.isActive() ?? false,
91
+ lastActivityAt: updatedAt && Number.isFinite(updatedAt.getTime()) ? updatedAt.toISOString() : null
92
+ };
93
+ }
84
94
  /**
85
95
  * The thread a message to a session should land on: the thread its role runs
86
96
  * on, else the thread it has open, else its most recently updated thread.
@@ -100,6 +110,6 @@ async function resolveSessionThread({ controller, sessions, workItems, sessionId
100
110
  return [...await controller.queryThreads({ resourceId: sessionId })].sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime())[0]?.id ?? null;
101
111
  }
102
112
  //#endregion
103
- export { FACTORY_SESSION_NOTIFICATION_SOURCE, deliverFactorySessionNotification, resolveSessionThread };
113
+ export { FACTORY_SESSION_NOTIFICATION_SOURCE, deliverFactorySessionNotification, resolveSessionThread, sessionActivity };
104
114
 
105
115
  //# sourceMappingURL=deliver.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"deliver.js","names":[],"sources":["../../src/tools/deliver.ts"],"sourcesContent":["import type { MountedMastraCode } from '@mastra/code-sdk';\nimport type { SendNotificationSignalInput } from '@mastra/core/notifications';\n\nimport { createSessionAsRowOwner } from '../integrations/subscription-session.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\n\n/** Notifications between Factory sessions all carry this source. */\nexport const FACTORY_SESSION_NOTIFICATION_SOURCE = 'factory-session';\n\nexport type FactoryDeliveryController = Pick<\n MountedMastraCode['controller'],\n | 'queryThreadById'\n | 'queryThreads'\n | 'setThreadModel'\n | 'listAvailableModels'\n | 'getSessionByResource'\n | 'createSession'\n | 'getCurrentAgent'\n>;\n\nexport interface FactoryDeliveryTarget {\n /** The target's source-control session id. */\n sessionId: string;\n /** The stored thread the notification belongs to. Never the session's current thread by default. */\n threadId: string;\n}\n\nexport type FactoryDeliveryResult =\n | { delivered: 'woken' | 'persisted' | 'summarized'; notificationId: string; reason?: string }\n | {\n delivered: 'skipped';\n notificationId?: string;\n reason: 'thread-not-found' | 'thread-not-owned' | 'session-not-found' | 'offline' | 'discarded';\n };\n\n/**\n * Deliver a notification to a Factory session's stored thread.\n *\n * Sent through the target's agent with an explicit `{ resourceId, threadId }`,\n * never `Session.sendNotificationSignal`: that one builds the wake from the\n * sender's request context and aims at the target's current thread. On this\n * path the SDK builds the wake from the session that owns the target resource,\n * so the woken run is the target row's owner, with that owner's credentials.\n *\n * A target that isn't live is created first, as its row's owner and bound to\n * the stored thread: waking a resource no session owns starts a run with no\n * model. A live session's thread is never switched.\n */\nexport async function deliverFactorySessionNotification({\n controller,\n sessions,\n target,\n notification,\n onlyIfLive = false,\n}: {\n controller: FactoryDeliveryController;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n target: FactoryDeliveryTarget;\n notification: Omit<SendNotificationSignalInput, 'source'>;\n /** Skip, without creating anything, when no live session owns the target (stage notices). */\n onlyIfLive?: boolean;\n}): Promise<FactoryDeliveryResult> {\n const thread = await controller.queryThreadById({ threadId: target.threadId });\n if (!thread) return { delivered: 'skipped', reason: 'thread-not-found' };\n // A Factory session's threads live under its session id. A thread owned by\n // anything else is not this session's, whatever the caller paired it with.\n if (thread.resourceId && thread.resourceId !== target.sessionId) {\n return { delivered: 'skipped', reason: 'thread-not-owned' };\n }\n const ownerResourceId = target.sessionId;\n\n // No request context: without a user the controller's access check is a no-op,\n // and this is a server-side lookup on the target's behalf, not the sender's.\n let session = await controller.getSessionByResource(ownerResourceId);\n if (!session) {\n if (onlyIfLive) return { delivered: 'skipped', reason: 'offline' };\n const row = await sessions.getBySessionId(target.sessionId);\n if (!row?.factoryProjectId) return { delivered: 'skipped', reason: 'session-not-found' };\n session = await createSessionAsRowOwner(controller, {\n sessionId: target.sessionId,\n ownerResourceId,\n row,\n tags: {\n factoryProjectId: row.factoryProjectId,\n ...(row.projectRepositoryId ? { projectRepositoryId: row.projectRepositoryId } : {}),\n },\n threadId: target.threadId,\n });\n }\n\n const result = await controller\n .getCurrentAgent(session)\n .sendNotificationSignal(\n { ...notification, source: FACTORY_SESSION_NOTIFICATION_SOURCE },\n { resourceId: ownerResourceId, threadId: target.threadId },\n );\n await Promise.all([result.persisted, result.accepted].filter(Boolean));\n\n const notificationId = result.record.id;\n const reason = result.decision.reason ? { reason: result.decision.reason } : {};\n switch (result.decision.action) {\n case 'deliver':\n return { delivered: 'woken', notificationId, ...reason };\n case 'summarize':\n return { delivered: 'summarized', notificationId, ...reason };\n case 'discard':\n return { delivered: 'skipped', notificationId, reason: 'discarded' };\n default:\n return { delivered: 'persisted', notificationId, ...reason };\n }\n}\n\n/**\n * The thread a message to a session should land on: the thread its role runs\n * on, else the thread it has open, else its most recently updated thread.\n */\nexport async function resolveSessionThread({\n controller,\n sessions,\n workItems,\n sessionId,\n}: {\n controller: Pick<FactoryDeliveryController, 'getSessionByResource' | 'queryThreads'>;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession'>;\n sessionId: string;\n}): Promise<string | null> {\n const row = await sessions.getBySessionId(sessionId);\n if (row?.factoryProjectId) {\n const binding = await workItems.findActiveRunBindingForSession({\n orgId: row.orgId,\n factoryProjectId: row.factoryProjectId,\n sessionId,\n });\n if (binding) return binding.threadId;\n }\n const live = await controller.getSessionByResource(sessionId);\n const current = live?.thread.getId();\n if (current) return current;\n const threads = await controller.queryThreads({ resourceId: sessionId });\n const newest = [...threads].sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime())[0];\n return newest?.id ?? null;\n}\n"],"mappings":";;;AAQA,MAAa,sCAAsC;;;;;;;;;;;;;;AAyCnD,eAAsB,kCAAkC,EACtD,YACA,UACA,QACA,cACA,aAAa,SAQoB;CACjC,MAAM,SAAS,MAAM,WAAW,gBAAgB,EAAE,UAAU,OAAO,SAAS,CAAC;CAC7E,IAAI,CAAC,QAAQ,OAAO;EAAE,WAAW;EAAW,QAAQ;CAAmB;CAGvE,IAAI,OAAO,cAAc,OAAO,eAAe,OAAO,WACpD,OAAO;EAAE,WAAW;EAAW,QAAQ;CAAmB;CAE5D,MAAM,kBAAkB,OAAO;CAI/B,IAAI,UAAU,MAAM,WAAW,qBAAqB,eAAe;CACnE,IAAI,CAAC,SAAS;EACZ,IAAI,YAAY,OAAO;GAAE,WAAW;GAAW,QAAQ;EAAU;EACjE,MAAM,MAAM,MAAM,SAAS,eAAe,OAAO,SAAS;EAC1D,IAAI,CAAC,KAAK,kBAAkB,OAAO;GAAE,WAAW;GAAW,QAAQ;EAAoB;EACvF,UAAU,MAAM,wBAAwB,YAAY;GAClD,WAAW,OAAO;GAClB;GACA;GACA,MAAM;IACJ,kBAAkB,IAAI;IACtB,GAAI,IAAI,sBAAsB,EAAE,qBAAqB,IAAI,oBAAoB,IAAI,CAAC;GACpF;GACA,UAAU,OAAO;EACnB,CAAC;CACH;CAEA,MAAM,SAAS,MAAM,WAClB,gBAAgB,OAAO,CAAC,CACxB,uBACC;EAAE,GAAG;EAAc,QAAQ;CAAoC,GAC/D;EAAE,YAAY;EAAiB,UAAU,OAAO;CAAS,CAC3D;CACF,MAAM,QAAQ,IAAI,CAAC,OAAO,WAAW,OAAO,QAAQ,CAAC,CAAC,OAAO,OAAO,CAAC;CAErE,MAAM,iBAAiB,OAAO,OAAO;CACrC,MAAM,SAAS,OAAO,SAAS,SAAS,EAAE,QAAQ,OAAO,SAAS,OAAO,IAAI,CAAC;CAC9E,QAAQ,OAAO,SAAS,QAAxB;EACE,KAAK,WACH,OAAO;GAAE,WAAW;GAAS;GAAgB,GAAG;EAAO;EACzD,KAAK,aACH,OAAO;GAAE,WAAW;GAAc;GAAgB,GAAG;EAAO;EAC9D,KAAK,WACH,OAAO;GAAE,WAAW;GAAW;GAAgB,QAAQ;EAAY;EACrE,SACE,OAAO;GAAE,WAAW;GAAa;GAAgB,GAAG;EAAO;CAC/D;AACF;;;;;AAMA,eAAsB,qBAAqB,EACzC,YACA,UACA,WACA,aAMyB;CACzB,MAAM,MAAM,MAAM,SAAS,eAAe,SAAS;CACnD,IAAI,KAAK,kBAAkB;EACzB,MAAM,UAAU,MAAM,UAAU,+BAA+B;GAC7D,OAAO,IAAI;GACX,kBAAkB,IAAI;GACtB;EACF,CAAC;EACD,IAAI,SAAS,OAAO,QAAQ;CAC9B;CAEA,MAAM,WAAU,MADG,WAAW,qBAAqB,SAAS,EAAA,EACtC,OAAO,MAAM;CACnC,IAAI,SAAS,OAAO;CAGpB,OADe,CAAC,GAAG,MADG,WAAW,aAAa,EAAE,YAAY,UAAU,CAAC,CAC7C,CAAC,CAAC,MAAM,MAAM,UAAU,MAAM,UAAU,QAAQ,IAAI,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,EAC3F,EAAE,MAAM;AACvB"}
1
+ {"version":3,"file":"deliver.js","names":[],"sources":["../../src/tools/deliver.ts"],"sourcesContent":["import type { MountedMastraCode } from '@mastra/code-sdk';\nimport type { SendNotificationSignalInput } from '@mastra/core/notifications';\n\nimport { createSessionAsRowOwner } from '../integrations/subscription-session.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\n\n/** Notifications between Factory sessions all carry this source. */\nexport const FACTORY_SESSION_NOTIFICATION_SOURCE = 'factory-session';\n\nexport type FactoryDeliveryController = Pick<\n MountedMastraCode['controller'],\n | 'queryThreadById'\n | 'queryThreads'\n | 'setThreadModel'\n | 'listAvailableModels'\n | 'getSessionByResource'\n | 'createSession'\n | 'getCurrentAgent'\n>;\n\nexport interface FactoryDeliveryTarget {\n /** The target's source-control session id. */\n sessionId: string;\n /** The stored thread the notification belongs to. Never the session's current thread by default. */\n threadId: string;\n}\n\nexport type FactoryDeliveryResult =\n /**\n * `woken`: the target was idle and the notification started a run.\n * `mid-run`: the target was already running; it reads the notification when that run\n * next checks its inbox, not now.\n */\n | { delivered: 'woken' | 'mid-run' | 'persisted' | 'summarized'; notificationId: string; reason?: string }\n | {\n delivered: 'skipped';\n notificationId?: string;\n reason: 'thread-not-found' | 'thread-not-owned' | 'session-not-found' | 'offline' | 'discarded';\n };\n\n/**\n * Deliver a notification to a Factory session's stored thread.\n *\n * Sent through the target's agent with an explicit `{ resourceId, threadId }`,\n * never `Session.sendNotificationSignal`: that one builds the wake from the\n * sender's request context and aims at the target's current thread. On this\n * path the SDK builds the wake from the session that owns the target resource,\n * so the woken run is the target row's owner, with that owner's credentials.\n *\n * A target that isn't live is created first, as its row's owner and bound to\n * the stored thread: waking a resource no session owns starts a run with no\n * model. A live session's thread is never switched.\n */\nexport async function deliverFactorySessionNotification({\n controller,\n sessions,\n target,\n notification,\n onlyIfLive = false,\n}: {\n controller: FactoryDeliveryController;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n target: FactoryDeliveryTarget;\n notification: Omit<SendNotificationSignalInput, 'source'>;\n /** Skip, without creating anything, when no live session owns the target (stage notices). */\n onlyIfLive?: boolean;\n}): Promise<FactoryDeliveryResult> {\n const thread = await controller.queryThreadById({ threadId: target.threadId });\n if (!thread) return { delivered: 'skipped', reason: 'thread-not-found' };\n // A Factory session's threads live under its session id. A thread owned by\n // anything else is not this session's, whatever the caller paired it with.\n if (thread.resourceId && thread.resourceId !== target.sessionId) {\n return { delivered: 'skipped', reason: 'thread-not-owned' };\n }\n const ownerResourceId = target.sessionId;\n\n // No request context: without a user the controller's access check is a no-op,\n // and this is a server-side lookup on the target's behalf, not the sender's.\n let session = await controller.getSessionByResource(ownerResourceId);\n if (!session) {\n if (onlyIfLive) return { delivered: 'skipped', reason: 'offline' };\n const row = await sessions.getBySessionId(target.sessionId);\n if (!row?.factoryProjectId) return { delivered: 'skipped', reason: 'session-not-found' };\n session = await createSessionAsRowOwner(controller, {\n sessionId: target.sessionId,\n ownerResourceId,\n row,\n tags: {\n factoryProjectId: row.factoryProjectId,\n ...(row.projectRepositoryId ? { projectRepositoryId: row.projectRepositoryId } : {}),\n },\n threadId: target.threadId,\n });\n }\n\n // Read before sending: the send itself starts a run on an idle target. Delivery is per thread,\n // so a run on another thread of the same session leaves the target thread idle.\n const busy = session.stream.isActive() && session.thread.getId() === target.threadId;\n const result = await controller\n .getCurrentAgent(session)\n .sendNotificationSignal(\n { ...notification, source: FACTORY_SESSION_NOTIFICATION_SOURCE },\n { resourceId: ownerResourceId, threadId: target.threadId },\n );\n await Promise.all([result.persisted, result.accepted].filter(Boolean));\n\n const notificationId = result.record.id;\n const reason = result.decision.reason ? { reason: result.decision.reason } : {};\n switch (result.decision.action) {\n case 'deliver':\n return { delivered: busy ? 'mid-run' : 'woken', notificationId, ...reason };\n case 'summarize':\n return { delivered: 'summarized', notificationId, ...reason };\n case 'discard':\n return { delivered: 'skipped', notificationId, reason: 'discarded' };\n default:\n return { delivered: 'persisted', notificationId, ...reason };\n }\n}\n\nexport interface FactorySessionActivity {\n /** Whether the session's agent is mid-run in this process. */\n running: boolean;\n /** When the session's thread last changed (a message saved, a run step persisted), ISO. */\n lastActivityAt: string | null;\n}\n\n/** Whether a session is busy right now, and when its thread last moved, so a stuck run is told apart from a long one. */\nexport async function sessionActivity(\n controller: Pick<FactoryDeliveryController, 'getSessionByResource' | 'queryThreadById'>,\n sessionId: string,\n threadId: string,\n): Promise<FactorySessionActivity> {\n const [live, thread] = await Promise.all([\n controller.getSessionByResource(sessionId),\n controller.queryThreadById({ threadId }),\n ]);\n const updatedAt = thread?.updatedAt ? new Date(thread.updatedAt) : null;\n return {\n running: live?.stream.isActive() ?? false,\n lastActivityAt: updatedAt && Number.isFinite(updatedAt.getTime()) ? updatedAt.toISOString() : null,\n };\n}\n\n/**\n * The thread a message to a session should land on: the thread its role runs\n * on, else the thread it has open, else its most recently updated thread.\n */\nexport async function resolveSessionThread({\n controller,\n sessions,\n workItems,\n sessionId,\n}: {\n controller: Pick<FactoryDeliveryController, 'getSessionByResource' | 'queryThreads'>;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession'>;\n sessionId: string;\n}): Promise<string | null> {\n const row = await sessions.getBySessionId(sessionId);\n if (row?.factoryProjectId) {\n const binding = await workItems.findActiveRunBindingForSession({\n orgId: row.orgId,\n factoryProjectId: row.factoryProjectId,\n sessionId,\n });\n if (binding) return binding.threadId;\n }\n const live = await controller.getSessionByResource(sessionId);\n const current = live?.thread.getId();\n if (current) return current;\n const threads = await controller.queryThreads({ resourceId: sessionId });\n const newest = [...threads].sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime())[0];\n return newest?.id ?? null;\n}\n"],"mappings":";;;AAQA,MAAa,sCAAsC;;;;;;;;;;;;;;AA8CnD,eAAsB,kCAAkC,EACtD,YACA,UACA,QACA,cACA,aAAa,SAQoB;CACjC,MAAM,SAAS,MAAM,WAAW,gBAAgB,EAAE,UAAU,OAAO,SAAS,CAAC;CAC7E,IAAI,CAAC,QAAQ,OAAO;EAAE,WAAW;EAAW,QAAQ;CAAmB;CAGvE,IAAI,OAAO,cAAc,OAAO,eAAe,OAAO,WACpD,OAAO;EAAE,WAAW;EAAW,QAAQ;CAAmB;CAE5D,MAAM,kBAAkB,OAAO;CAI/B,IAAI,UAAU,MAAM,WAAW,qBAAqB,eAAe;CACnE,IAAI,CAAC,SAAS;EACZ,IAAI,YAAY,OAAO;GAAE,WAAW;GAAW,QAAQ;EAAU;EACjE,MAAM,MAAM,MAAM,SAAS,eAAe,OAAO,SAAS;EAC1D,IAAI,CAAC,KAAK,kBAAkB,OAAO;GAAE,WAAW;GAAW,QAAQ;EAAoB;EACvF,UAAU,MAAM,wBAAwB,YAAY;GAClD,WAAW,OAAO;GAClB;GACA;GACA,MAAM;IACJ,kBAAkB,IAAI;IACtB,GAAI,IAAI,sBAAsB,EAAE,qBAAqB,IAAI,oBAAoB,IAAI,CAAC;GACpF;GACA,UAAU,OAAO;EACnB,CAAC;CACH;CAIA,MAAM,OAAO,QAAQ,OAAO,SAAS,KAAK,QAAQ,OAAO,MAAM,MAAM,OAAO;CAC5E,MAAM,SAAS,MAAM,WAClB,gBAAgB,OAAO,CAAC,CACxB,uBACC;EAAE,GAAG;EAAc,QAAQ;CAAoC,GAC/D;EAAE,YAAY;EAAiB,UAAU,OAAO;CAAS,CAC3D;CACF,MAAM,QAAQ,IAAI,CAAC,OAAO,WAAW,OAAO,QAAQ,CAAC,CAAC,OAAO,OAAO,CAAC;CAErE,MAAM,iBAAiB,OAAO,OAAO;CACrC,MAAM,SAAS,OAAO,SAAS,SAAS,EAAE,QAAQ,OAAO,SAAS,OAAO,IAAI,CAAC;CAC9E,QAAQ,OAAO,SAAS,QAAxB;EACE,KAAK,WACH,OAAO;GAAE,WAAW,OAAO,YAAY;GAAS;GAAgB,GAAG;EAAO;EAC5E,KAAK,aACH,OAAO;GAAE,WAAW;GAAc;GAAgB,GAAG;EAAO;EAC9D,KAAK,WACH,OAAO;GAAE,WAAW;GAAW;GAAgB,QAAQ;EAAY;EACrE,SACE,OAAO;GAAE,WAAW;GAAa;GAAgB,GAAG;EAAO;CAC/D;AACF;;AAUA,eAAsB,gBACpB,YACA,WACA,UACiC;CACjC,MAAM,CAAC,MAAM,UAAU,MAAM,QAAQ,IAAI,CACvC,WAAW,qBAAqB,SAAS,GACzC,WAAW,gBAAgB,EAAE,SAAS,CAAC,CACzC,CAAC;CACD,MAAM,YAAY,QAAQ,YAAY,IAAI,KAAK,OAAO,SAAS,IAAI;CACnE,OAAO;EACL,SAAS,MAAM,OAAO,SAAS,KAAK;EACpC,gBAAgB,aAAa,OAAO,SAAS,UAAU,QAAQ,CAAC,IAAI,UAAU,YAAY,IAAI;CAChG;AACF;;;;;AAMA,eAAsB,qBAAqB,EACzC,YACA,UACA,WACA,aAMyB;CACzB,MAAM,MAAM,MAAM,SAAS,eAAe,SAAS;CACnD,IAAI,KAAK,kBAAkB;EACzB,MAAM,UAAU,MAAM,UAAU,+BAA+B;GAC7D,OAAO,IAAI;GACX,kBAAkB,IAAI;GACtB;EACF,CAAC;EACD,IAAI,SAAS,OAAO,QAAQ;CAC9B;CAEA,MAAM,WAAU,MADG,WAAW,qBAAqB,SAAS,EAAA,EACtC,OAAO,MAAM;CACnC,IAAI,SAAS,OAAO;CAGpB,OADe,CAAC,GAAG,MADG,WAAW,aAAa,EAAE,YAAY,UAAU,CAAC,CAC7C,CAAC,CAAC,MAAM,MAAM,UAAU,MAAM,UAAU,QAAQ,IAAI,KAAK,UAAU,QAAQ,CAAC,CAAC,CAAC,EAC3F,EAAE,MAAM;AACvB"}
@@ -19,11 +19,12 @@ export interface CreateFactoryToolsOptions {
19
19
  * closure and never read the request context, so a non-session transport
20
20
  * (an MCP server) can mount the same set with a scope it resolved itself.
21
21
  *
22
- * Worker and side sessions only answer messages. User sessions get everything; turns
23
- * nobody signed in started (notification wakes, webhooks, dispatcher notices)
24
- * get the read tools and messaging limited to sessions they follow: text from
25
- * other people reaches those turns, and the management tools act as the
26
- * session owner.
22
+ * Worker and side sessions only answer messages. User sessions get every tool
23
+ * on every turn. Turns nobody signed in started (notification wakes, webhooks,
24
+ * dispatcher notices) carry text other people wrote, so each tool limits what
25
+ * it does on them itself: approvals, proposals, plan pre-approval and muting
26
+ * side sessions need a person, moves run as an agent, model switches reach
27
+ * only this session and its side sessions, and starts are side sessions only.
27
28
  */
28
29
  export declare function createFactoryTools({ scope, deps }: CreateFactoryToolsOptions): IntegrationTools;
29
30
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,qBAAqB,CAAC;AAE3E,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,sBAAsB,CAAC;AAE7E,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAEzE,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,8BAA8B,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAC9E,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,0DAA0D;AAC1D,MAAM,MAAM,uBAAuB,GAAG,2BAA2B,GAC/D,+BAA+B,GAC/B,gCAAgC,GAChC,8BAA8B,GAC9B,+BAA+B,CAAC;AAElC,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,gBAAgB,CAAC;IACxB,IAAI,EAAE,uBAAuB,CAAC;CAC/B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,yBAAyB,GAAG,gBAAgB,CAW/F"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEhE,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,qBAAqB,CAAC;AAE3E,OAAO,KAAK,EAAE,gCAAgC,EAAE,MAAM,sBAAsB,CAAC;AAE7E,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,oBAAoB,CAAC;AAEzE,OAAO,KAAK,EAAE,+BAA+B,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EAAE,8BAA8B,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAC/F,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAC9E,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,0DAA0D;AAC1D,MAAM,MAAM,uBAAuB,GAAG,2BAA2B,GAC/D,+BAA+B,GAC/B,gCAAgC,GAChC,8BAA8B,GAC9B,+BAA+B,CAAC;AAElC,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,gBAAgB,CAAC;IACxB,IAAI,EAAE,uBAAuB,CAAC;CAC/B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,yBAAyB,GAAG,gBAAgB,CAU/F"}
@@ -1,33 +1,30 @@
1
1
  import { FACTORY_INTERACTIVE_KEY } from "../auth.js";
2
+ import { resolveFactoryToolScope } from "./scope.js";
2
3
  import { createFactoryApprovalTools } from "./approval-tools.js";
3
4
  import { createFactoryReadTools } from "./read-tools.js";
4
5
  import { createFactoryMessagingTools } from "./messaging-tools.js";
5
6
  import { createFactorySessionTools } from "./session-tools.js";
6
7
  import { createFactoryWorkItemTools } from "./work-item-tools.js";
7
8
  import { FACTORY_TOOLS_WITHOUT_APPROVAL, applyFactoryToolPermissions } from "./permissions.js";
8
- import { resolveFactoryToolScope } from "./scope.js";
9
9
  //#region src/tools/index.ts
10
10
  /**
11
11
  * The Factory toolset for a resolved scope. Tools take scope and storage by
12
12
  * closure and never read the request context, so a non-session transport
13
13
  * (an MCP server) can mount the same set with a scope it resolved itself.
14
14
  *
15
- * Worker and side sessions only answer messages. User sessions get everything; turns
16
- * nobody signed in started (notification wakes, webhooks, dispatcher notices)
17
- * get the read tools and messaging limited to sessions they follow: text from
18
- * other people reaches those turns, and the management tools act as the
19
- * session owner.
15
+ * Worker and side sessions only answer messages. User sessions get every tool
16
+ * on every turn. Turns nobody signed in started (notification wakes, webhooks,
17
+ * dispatcher notices) carry text other people wrote, so each tool limits what
18
+ * it does on them itself: approvals, proposals, plan pre-approval and muting
19
+ * side sessions need a person, moves run as an agent, model switches reach
20
+ * only this session and its side sessions, and starts are side sessions only.
20
21
  */
21
22
  function createFactoryTools({ scope, deps }) {
22
23
  const messagingTools = createFactoryMessagingTools(scope, deps);
23
24
  if (scope.kind !== "user") return messagingTools;
24
- const readTools = {
25
- ...createFactoryReadTools(scope, deps),
26
- ...messagingTools
27
- };
28
- if (!scope.interactive) return readTools;
29
25
  return {
30
- ...readTools,
26
+ ...createFactoryReadTools(scope, deps),
27
+ ...messagingTools,
31
28
  ...createFactoryWorkItemTools(scope, deps),
32
29
  ...createFactorySessionTools(scope, deps),
33
30
  ...createFactoryApprovalTools(scope, deps)
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/tools/index.ts"],"sourcesContent":["import type { IntegrationTools } from '../integrations/base.js';\nimport { createFactoryApprovalTools } from './approval-tools.js';\nimport type { FactoryApprovalToolDependencies } from './approval-tools.js';\nimport { createFactoryMessagingTools } from './messaging-tools.js';\nimport type { FactoryMessagingToolDependencies } from './messaging-tools.js';\nimport { createFactoryReadTools } from './read-tools.js';\nimport type { FactoryReadToolDependencies } from './read-tools.js';\nimport type { FactoryToolScope } from './scope.js';\nimport { createFactorySessionTools } from './session-tools.js';\nimport type { FactorySessionToolDependencies } from './session-tools.js';\nimport { createFactoryWorkItemTools } from './work-item-tools.js';\nimport type { FactoryWorkItemToolDependencies } from './work-item-tools.js';\n\nexport { FACTORY_TOOLS_WITHOUT_APPROVAL, applyFactoryToolPermissions } from './permissions.js';\nexport { FACTORY_INTERACTIVE_KEY, resolveFactoryToolScope } from './scope.js';\nexport type { FactoryToolScope } from './scope.js';\n\n/** Storage and services the Factory tools act through. */\nexport type FactoryToolDependencies = FactoryReadToolDependencies &\n FactoryWorkItemToolDependencies &\n FactoryMessagingToolDependencies &\n FactorySessionToolDependencies &\n FactoryApprovalToolDependencies;\n\nexport interface CreateFactoryToolsOptions {\n scope: FactoryToolScope;\n deps: FactoryToolDependencies;\n}\n\n/**\n * The Factory toolset for a resolved scope. Tools take scope and storage by\n * closure and never read the request context, so a non-session transport\n * (an MCP server) can mount the same set with a scope it resolved itself.\n *\n * Worker and side sessions only answer messages. User sessions get everything; turns\n * nobody signed in started (notification wakes, webhooks, dispatcher notices)\n * get the read tools and messaging limited to sessions they follow: text from\n * other people reaches those turns, and the management tools act as the\n * session owner.\n */\nexport function createFactoryTools({ scope, deps }: CreateFactoryToolsOptions): IntegrationTools {\n const messagingTools = createFactoryMessagingTools(scope, deps);\n if (scope.kind !== 'user') return messagingTools;\n const readTools = { ...createFactoryReadTools(scope, deps), ...messagingTools };\n if (!scope.interactive) return readTools;\n return {\n ...readTools,\n ...createFactoryWorkItemTools(scope, deps),\n ...createFactorySessionTools(scope, deps),\n ...createFactoryApprovalTools(scope, deps),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAwCA,SAAgB,mBAAmB,EAAE,OAAO,QAAqD;CAC/F,MAAM,iBAAiB,4BAA4B,OAAO,IAAI;CAC9D,IAAI,MAAM,SAAS,QAAQ,OAAO;CAClC,MAAM,YAAY;EAAE,GAAG,uBAAuB,OAAO,IAAI;EAAG,GAAG;CAAe;CAC9E,IAAI,CAAC,MAAM,aAAa,OAAO;CAC/B,OAAO;EACL,GAAG;EACH,GAAG,2BAA2B,OAAO,IAAI;EACzC,GAAG,0BAA0B,OAAO,IAAI;EACxC,GAAG,2BAA2B,OAAO,IAAI;CAC3C;AACF"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/tools/index.ts"],"sourcesContent":["import type { IntegrationTools } from '../integrations/base.js';\nimport { createFactoryApprovalTools } from './approval-tools.js';\nimport type { FactoryApprovalToolDependencies } from './approval-tools.js';\nimport { createFactoryMessagingTools } from './messaging-tools.js';\nimport type { FactoryMessagingToolDependencies } from './messaging-tools.js';\nimport { createFactoryReadTools } from './read-tools.js';\nimport type { FactoryReadToolDependencies } from './read-tools.js';\nimport type { FactoryToolScope } from './scope.js';\nimport { createFactorySessionTools } from './session-tools.js';\nimport type { FactorySessionToolDependencies } from './session-tools.js';\nimport { createFactoryWorkItemTools } from './work-item-tools.js';\nimport type { FactoryWorkItemToolDependencies } from './work-item-tools.js';\n\nexport { FACTORY_TOOLS_WITHOUT_APPROVAL, applyFactoryToolPermissions } from './permissions.js';\nexport { FACTORY_INTERACTIVE_KEY, resolveFactoryToolScope } from './scope.js';\nexport type { FactoryToolScope } from './scope.js';\n\n/** Storage and services the Factory tools act through. */\nexport type FactoryToolDependencies = FactoryReadToolDependencies &\n FactoryWorkItemToolDependencies &\n FactoryMessagingToolDependencies &\n FactorySessionToolDependencies &\n FactoryApprovalToolDependencies;\n\nexport interface CreateFactoryToolsOptions {\n scope: FactoryToolScope;\n deps: FactoryToolDependencies;\n}\n\n/**\n * The Factory toolset for a resolved scope. Tools take scope and storage by\n * closure and never read the request context, so a non-session transport\n * (an MCP server) can mount the same set with a scope it resolved itself.\n *\n * Worker and side sessions only answer messages. User sessions get every tool\n * on every turn. Turns nobody signed in started (notification wakes, webhooks,\n * dispatcher notices) carry text other people wrote, so each tool limits what\n * it does on them itself: approvals, proposals, plan pre-approval and muting\n * side sessions need a person, moves run as an agent, model switches reach\n * only this session and its side sessions, and starts are side sessions only.\n */\nexport function createFactoryTools({ scope, deps }: CreateFactoryToolsOptions): IntegrationTools {\n const messagingTools = createFactoryMessagingTools(scope, deps);\n if (scope.kind !== 'user') return messagingTools;\n return {\n ...createFactoryReadTools(scope, deps),\n ...messagingTools,\n ...createFactoryWorkItemTools(scope, deps),\n ...createFactorySessionTools(scope, deps),\n ...createFactoryApprovalTools(scope, deps),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,mBAAmB,EAAE,OAAO,QAAqD;CAC/F,MAAM,iBAAiB,4BAA4B,OAAO,IAAI;CAC9D,IAAI,MAAM,SAAS,QAAQ,OAAO;CAClC,OAAO;EACL,GAAG,uBAAuB,OAAO,IAAI;EACrC,GAAG;EACH,GAAG,2BAA2B,OAAO,IAAI;EACzC,GAAG,0BAA0B,OAAO,IAAI;EACxC,GAAG,2BAA2B,OAAO,IAAI;CAC3C;AACF"}
@@ -18,7 +18,7 @@ export interface FactoryMessaging {
18
18
  notifications: Pick<NotificationsStorage, 'listNotifications' | 'getNotification'>;
19
19
  }
20
20
  export interface FactoryMessagingToolDependencies {
21
- sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'listByOrigin'>;
21
+ sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'listByOrigin' | 'setOriginMuted'>;
22
22
  workItems: Pick<WorkItemsStorage, 'findActiveRunBindingForSession' | 'getForProject' | 'listSessionSubscriptions' | 'listRunBindings' | 'listWorkItemSubscribers'>;
23
23
  auditRecorder: AuditRecorder;
24
24
  /** The live controller and notifications store; resolved per call because they exist only once the mount is up. */
@@ -1 +1 @@
1
- {"version":3,"file":"messaging-tools.d.ts","sourceRoot":"","sources":["../../src/tools/messaging-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAA4C,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAIjH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAExE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAM9E,OAAO,KAAK,EAAE,yBAAyB,EAAyB,MAAM,cAAc,CAAC;AAGrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,gFAAgF;AAChF,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,qBAAqB,QAAiB,CAAC;AAKpD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,yBAAyB,CAAC;IACtC,aAAa,EAAE,IAAI,CAAC,oBAAoB,EAAE,mBAAmB,GAAG,iBAAiB,CAAC,CAAC;CACpF;AAED,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,cAAc,CAAC,CAAC;IAC9E,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,gCAAgC,GAChC,eAAe,GACf,0BAA0B,GAC1B,iBAAiB,GACjB,yBAAyB,CAC5B,CAAC;IACF,aAAa,EAAE,aAAa,CAAC;IAC7B,mHAAmH;IACnH,SAAS,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CACnD;AAgBD,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,gCAAgC,GACrC,gBAAgB,CAyRlB"}
1
+ {"version":3,"file":"messaging-tools.d.ts","sourceRoot":"","sources":["../../src/tools/messaging-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAA4C,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAIjH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AAExE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAM9E,OAAO,KAAK,EAAE,yBAAyB,EAAyB,MAAM,cAAc,CAAC;AAIrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGnD,gFAAgF;AAChF,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,qBAAqB,QAAiB,CAAC;AAKpD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,yBAAyB,CAAC;IACtC,aAAa,EAAE,IAAI,CAAC,oBAAoB,EAAE,mBAAmB,GAAG,iBAAiB,CAAC,CAAC;CACpF;AAED,MAAM,WAAW,gCAAgC;IAC/C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,cAAc,GAAG,gBAAgB,CAAC,CAAC;IACjG,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,gCAAgC,GAChC,eAAe,GACf,0BAA0B,GAC1B,iBAAiB,GACjB,yBAAyB,CAC5B,CAAC;IACF,aAAa,EAAE,aAAa,CAAC;IAC7B,mHAAmH;IACnH,SAAS,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CACnD;AAgBD,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,gCAAgC,GACrC,gBAAgB,CAyXlB"}
@@ -1,6 +1,8 @@
1
+ import { requirePerson } from "./scope.js";
1
2
  import { FACTORY_SESSION_NOTIFICATION_SOURCE, deliverFactorySessionNotification, resolveSessionThread } from "./deliver.js";
2
3
  import { workItemNumber } from "./read-tools.js";
3
4
  import { reportNotification, workItemReportAttributes, workItemReportRecipients } from "./reports.js";
5
+ import { listSideSessions, sessionModel } from "./side-sessions.js";
4
6
  import { z } from "zod";
5
7
  import { createTool } from "@mastra/core/tools";
6
8
  //#region src/tools/messaging-tools.ts
@@ -96,6 +98,7 @@ function createFactoryMessagingTools(scope, deps) {
96
98
  "Send a message to another session in this Factory: a work item role session, or a teammate session you can see.",
97
99
  "Replies come back to this session as `factory-session` notifications with kind `reply` and an `in-reply-to` attribute naming your message.",
98
100
  "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.",
101
+ "`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.",
99
102
  ...scope.interactive ? [] : ["On this turn you can only message your side sessions and sessions working on items this session follows."]
100
103
  ].join(" "),
101
104
  inputSchema: z.object({
@@ -234,8 +237,15 @@ function createFactoryMessagingTools(scope, deps) {
234
237
  };
235
238
  let recipients;
236
239
  let attributes = {};
237
- if (scope.origin) recipients = [scope.origin];
238
- else {
240
+ if (scope.origin) {
241
+ if ((await deps.sessions.getBySessionId(scope.sessionId))?.originMutedAt && status !== "blocked") return {
242
+ delivered: 0,
243
+ recipients: [],
244
+ muted: true,
245
+ note: "Your origin muted your reports; keep working."
246
+ };
247
+ recipients = [scope.origin];
248
+ } else {
239
249
  const binding = await deps.workItems.findActiveRunBindingForSession({
240
250
  orgId: scope.orgId,
241
251
  factoryProjectId: scope.factoryProjectId,
@@ -300,10 +310,81 @@ function createFactoryMessagingTools(scope, deps) {
300
310
  recipients: results
301
311
  };
302
312
  }
313
+ const listSide = createTool({
314
+ id: "factory_list_side_sessions",
315
+ description: ["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.", "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."].join(" "),
316
+ inputSchema: z.object({
317
+ state: z.enum([
318
+ "running",
319
+ "parked",
320
+ "idle"
321
+ ]).optional().describe("Only sessions in this state."),
322
+ muted: z.boolean().optional().describe("true for only muted sessions, false for only unmuted ones.")
323
+ }).strict(),
324
+ execute: async ({ state, muted }) => {
325
+ const messaging = await deps.messaging();
326
+ return { sessions: (await listSideSessions({
327
+ sessions: deps.sessions,
328
+ workItems: deps.workItems,
329
+ notifications: messaging?.notifications ?? null,
330
+ isRunning: async (sessionId) => (await messaging?.controller.getSessionByResource(sessionId))?.stream.isActive() ?? false,
331
+ model: messaging ? (sessionId) => sessionModel(messaging.controller, deps.sessions, deps.workItems, sessionId) : void 0
332
+ }, scope)).filter((row) => (!state || row.state === state) && (muted === void 0 || row.mutedAt !== null === muted)) };
333
+ }
334
+ });
335
+ /** A side session this session started, or an error naming why it isn't one. */
336
+ const ownSideSession = async (sessionId) => {
337
+ const row = await deps.sessions.getBySessionId(sessionId);
338
+ if (!row || row.orgId !== scope.orgId || row.factoryProjectId !== scope.factoryProjectId || row.originSessionId !== scope.sessionId) throw new Error(`Session ${sessionId} is not a side session you started; see factory_list_side_sessions.`);
339
+ return row;
340
+ };
341
+ const sideSessionId = z.string().trim().min(1).max(200).describe("The side session, from factory_list_side_sessions.");
342
+ const unsubscribe = createTool({
343
+ id: "factory_unsubscribe_session",
344
+ description: "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.",
345
+ inputSchema: z.object({ sessionId: sideSessionId }).strict(),
346
+ execute: async ({ sessionId }) => {
347
+ requirePerson(scope, "mute a side session");
348
+ const row = await ownSideSession(sessionId);
349
+ if (row.originMutedAt) return {
350
+ sessionId,
351
+ muted: true,
352
+ mutedAt: row.originMutedAt.toISOString()
353
+ };
354
+ await deps.sessions.setOriginMuted({
355
+ sessionId,
356
+ muted: true
357
+ });
358
+ return {
359
+ sessionId,
360
+ muted: true,
361
+ mutedAt: (await deps.sessions.getBySessionId(sessionId))?.originMutedAt?.toISOString() ?? null
362
+ };
363
+ }
364
+ });
365
+ const subscribe = createTool({
366
+ id: "factory_subscribe_session",
367
+ description: "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.",
368
+ inputSchema: z.object({ sessionId: sideSessionId }).strict(),
369
+ execute: async ({ sessionId }) => {
370
+ requirePerson(scope, "unmute a side session");
371
+ if ((await ownSideSession(sessionId)).originMutedAt) await deps.sessions.setOriginMuted({
372
+ sessionId,
373
+ muted: false
374
+ });
375
+ return {
376
+ sessionId,
377
+ muted: false
378
+ };
379
+ }
380
+ });
303
381
  if (scope.kind === "worker") return { factory_message_origin: reply };
304
382
  return {
305
383
  factory_signal_session: signal,
306
- factory_message_origin: reply
384
+ factory_message_origin: reply,
385
+ factory_list_side_sessions: listSide,
386
+ factory_unsubscribe_session: unsubscribe,
387
+ factory_subscribe_session: subscribe
307
388
  };
308
389
  }
309
390
  //#endregion
@@ -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 type { FactoryToolScope } from './scope.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'>;\n workItems: Pick<\n WorkItemsStorage,\n | 'findActiveRunBindingForSession'\n | 'getForProject'\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 ...(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 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 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 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 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 if (scope.kind === 'worker') return { factory_message_origin: reply };\n return { factory_signal_session: signal, factory_message_origin: reply };\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 { 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 | '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 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 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 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 =>\n (await messaging?.controller.getSessionByResource(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 +1 @@
1
- {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/tools/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEvF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,EAAE,SAAS,MAAM,EAiB3D,CAAC;AAEF,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,QAAQ,CAAC,WAAW,EAAE;QACpB,QAAQ,IAAI,eAAe,CAAC;QAC5B,UAAU,CAAC,IAAI,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,gBAAgB,CAAA;SAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACjF,CAAC;CACH;AAED,MAAM,WAAW,kCAAkC;IACjD,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;CAC9D;AAED;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,6BAA6B,EACtC,EAAE,QAAQ,EAAE,EAAE,kCAAkC,EAChD,SAAS,GAAE,SAAS,MAAM,EAAmC,GAC5D,OAAO,CAAC,IAAI,CAAC,CASf"}
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/tools/permissions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAEvF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,EAAE,SAAS,MAAM,EAoB3D,CAAC;AAEF,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,QAAQ,CAAC,WAAW,EAAE;QACpB,QAAQ,IAAI,eAAe,CAAC;QAC5B,UAAU,CAAC,IAAI,EAAE;YAAE,QAAQ,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,gBAAgB,CAAA;SAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACjF,CAAC;CACH;AAED,MAAM,WAAW,kCAAkC;IACjD,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,CAAC,CAAC;CAC9D;AAED;;;;;GAKG;AACH,wBAAsB,2BAA2B,CAC/C,OAAO,EAAE,6BAA6B,EACtC,EAAE,QAAQ,EAAE,EAAE,kCAAkC,EAChD,SAAS,GAAE,SAAS,MAAM,EAAmC,GAC5D,OAAO,CAAC,IAAI,CAAC,CASf"}
@@ -18,6 +18,9 @@ const FACTORY_TOOLS_WITHOUT_APPROVAL = [
18
18
  "factory_unsubscribe_work_item",
19
19
  "factory_signal_session",
20
20
  "factory_message_origin",
21
+ "factory_list_side_sessions",
22
+ "factory_subscribe_session",
23
+ "factory_unsubscribe_session",
21
24
  "factory_start_session",
22
25
  "factory_answer_approval",
23
26
  "factory_switch_session_model"
@@ -1 +1 @@
1
- {"version":3,"file":"permissions.js","names":[],"sources":["../../src/tools/permissions.ts"],"sourcesContent":["import type { PermissionPolicy, PermissionRules } from '@mastra/core/agent-controller';\n\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\n\n/**\n * Factory tools that run without asking. Factory tools fall into the SDK's\n * `mcp` category, whose default policy is `ask`, so each of these needs an\n * explicit per-session `allow`.\n */\nexport const FACTORY_TOOLS_WITHOUT_APPROVAL: readonly string[] = [\n 'factory_list_work_items',\n 'factory_inspect_work_item',\n 'factory_list_attention',\n 'factory_create_work_item',\n 'factory_update_work_item',\n 'factory_transition_work_item',\n 'factory_retry_decision',\n 'factory_dismiss_decision',\n 'factory_resolve_proposal',\n 'factory_subscribe_work_item',\n 'factory_unsubscribe_work_item',\n 'factory_signal_session',\n 'factory_message_origin',\n 'factory_start_session',\n 'factory_answer_approval',\n 'factory_switch_session_model',\n];\n\nexport interface FactoryToolPermissionsSession {\n readonly identity: { getResourceId(): string };\n readonly permissions: {\n getRules(): PermissionRules;\n setForTool(args: { toolName: string; policy: PermissionPolicy }): Promise<void>;\n };\n}\n\nexport interface FactoryToolPermissionsDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n}\n\n/**\n * Allow the no-approval Factory tools on a newly created Factory session.\n * This only sets policy; which tools a turn actually gets is decided by the\n * tool scope. A person's own choices win: a per-tool policy is never\n * overwritten, and an explicit `mcp` category policy leaves everything alone.\n */\nexport async function applyFactoryToolPermissions(\n session: FactoryToolPermissionsSession,\n { sessions }: FactoryToolPermissionsDependencies,\n toolNames: readonly string[] = FACTORY_TOOLS_WITHOUT_APPROVAL,\n): Promise<void> {\n const row = await sessions.getBySessionId(session.identity.getResourceId());\n if (!row?.factoryProjectId) return;\n const rules = session.permissions.getRules();\n if (rules.categories.mcp !== undefined) return;\n for (const toolName of toolNames) {\n if (session.permissions.getRules().tools[toolName] !== undefined) continue;\n await session.permissions.setForTool({ toolName, policy: 'allow' });\n }\n}\n"],"mappings":";;;;;;AASA,MAAa,iCAAoD;CAC/D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;AAoBA,eAAsB,4BACpB,SACA,EAAE,YACF,YAA+B,gCAChB;CAEf,IAAI,EAAC,MADa,SAAS,eAAe,QAAQ,SAAS,cAAc,CAAC,EAAA,EAChE,kBAAkB;CAE5B,IADc,QAAQ,YAAY,SAC1B,CAAC,CAAC,WAAW,QAAQ,KAAA,GAAW;CACxC,KAAK,MAAM,YAAY,WAAW;EAChC,IAAI,QAAQ,YAAY,SAAS,CAAC,CAAC,MAAM,cAAc,KAAA,GAAW;EAClE,MAAM,QAAQ,YAAY,WAAW;GAAE;GAAU,QAAQ;EAAQ,CAAC;CACpE;AACF"}
1
+ {"version":3,"file":"permissions.js","names":[],"sources":["../../src/tools/permissions.ts"],"sourcesContent":["import type { PermissionPolicy, PermissionRules } from '@mastra/core/agent-controller';\n\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\n\n/**\n * Factory tools that run without asking. Factory tools fall into the SDK's\n * `mcp` category, whose default policy is `ask`, so each of these needs an\n * explicit per-session `allow`.\n */\nexport const FACTORY_TOOLS_WITHOUT_APPROVAL: readonly string[] = [\n 'factory_list_work_items',\n 'factory_inspect_work_item',\n 'factory_list_attention',\n 'factory_create_work_item',\n 'factory_update_work_item',\n 'factory_transition_work_item',\n 'factory_retry_decision',\n 'factory_dismiss_decision',\n 'factory_resolve_proposal',\n 'factory_subscribe_work_item',\n 'factory_unsubscribe_work_item',\n 'factory_signal_session',\n 'factory_message_origin',\n 'factory_list_side_sessions',\n 'factory_subscribe_session',\n 'factory_unsubscribe_session',\n 'factory_start_session',\n 'factory_answer_approval',\n 'factory_switch_session_model',\n];\n\nexport interface FactoryToolPermissionsSession {\n readonly identity: { getResourceId(): string };\n readonly permissions: {\n getRules(): PermissionRules;\n setForTool(args: { toolName: string; policy: PermissionPolicy }): Promise<void>;\n };\n}\n\nexport interface FactoryToolPermissionsDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId'>;\n}\n\n/**\n * Allow the no-approval Factory tools on a newly created Factory session.\n * This only sets policy; which tools a turn actually gets is decided by the\n * tool scope. A person's own choices win: a per-tool policy is never\n * overwritten, and an explicit `mcp` category policy leaves everything alone.\n */\nexport async function applyFactoryToolPermissions(\n session: FactoryToolPermissionsSession,\n { sessions }: FactoryToolPermissionsDependencies,\n toolNames: readonly string[] = FACTORY_TOOLS_WITHOUT_APPROVAL,\n): Promise<void> {\n const row = await sessions.getBySessionId(session.identity.getResourceId());\n if (!row?.factoryProjectId) return;\n const rules = session.permissions.getRules();\n if (rules.categories.mcp !== undefined) return;\n for (const toolName of toolNames) {\n if (session.permissions.getRules().tools[toolName] !== undefined) continue;\n await session.permissions.setForTool({ toolName, policy: 'allow' });\n }\n}\n"],"mappings":";;;;;;AASA,MAAa,iCAAoD;CAC/D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;AAoBA,eAAsB,4BACpB,SACA,EAAE,YACF,YAA+B,gCAChB;CAEf,IAAI,EAAC,MADa,SAAS,eAAe,QAAQ,SAAS,cAAc,CAAC,EAAA,EAChE,kBAAkB;CAE5B,IADc,QAAQ,YAAY,SAC1B,CAAC,CAAC,WAAW,QAAQ,KAAA,GAAW;CACxC,KAAK,MAAM,YAAY,WAAW;EAChC,IAAI,QAAQ,YAAY,SAAS,CAAC,CAAC,MAAM,cAAc,KAAA,GAAW;EAClE,MAAM,QAAQ,YAAY,WAAW;GAAE;GAAU,QAAQ;EAAQ,CAAC;CACpE;AACF"}