@mastra/factory 0.26.0-alpha.2 → 0.26.0-alpha.3

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 (154) hide show
  1. package/README.md +74 -7
  2. package/dist/boards/define-board.d.ts +19 -0
  3. package/dist/boards/define-board.d.ts.map +1 -1
  4. package/dist/boards/define-board.js +41 -9
  5. package/dist/boards/define-board.js.map +1 -1
  6. package/dist/boards/project-boards.d.ts +78 -0
  7. package/dist/boards/project-boards.d.ts.map +1 -0
  8. package/dist/boards/project-boards.js +194 -0
  9. package/dist/boards/project-boards.js.map +1 -0
  10. package/dist/boards/review.js +2 -2
  11. package/dist/boards/review.js.map +1 -1
  12. package/dist/boards/stored-board.d.ts +101 -0
  13. package/dist/boards/stored-board.d.ts.map +1 -0
  14. package/dist/boards/stored-board.js +139 -0
  15. package/dist/boards/stored-board.js.map +1 -0
  16. package/dist/boards/work.d.ts.map +1 -1
  17. package/dist/boards/work.js +6 -3
  18. package/dist/boards/work.js.map +1 -1
  19. package/dist/capabilities/intake.d.ts +6 -0
  20. package/dist/capabilities/intake.d.ts.map +1 -1
  21. package/dist/factory.d.ts +15 -2
  22. package/dist/factory.d.ts.map +1 -1
  23. package/dist/factory.js +49 -10
  24. package/dist/factory.js.map +1 -1
  25. package/dist/integrations/jira/binding-scope.d.ts +30 -0
  26. package/dist/integrations/jira/binding-scope.d.ts.map +1 -0
  27. package/dist/integrations/jira/binding-scope.js +50 -0
  28. package/dist/integrations/jira/binding-scope.js.map +1 -0
  29. package/dist/integrations/jira/claim.d.ts +12 -0
  30. package/dist/integrations/jira/claim.d.ts.map +1 -1
  31. package/dist/integrations/jira/claim.js +24 -1
  32. package/dist/integrations/jira/claim.js.map +1 -1
  33. package/dist/integrations/jira/intake-jql.d.ts +32 -0
  34. package/dist/integrations/jira/intake-jql.d.ts.map +1 -0
  35. package/dist/integrations/jira/intake-jql.js +57 -0
  36. package/dist/integrations/jira/intake-jql.js.map +1 -0
  37. package/dist/integrations/jira/integration.d.ts +4 -3
  38. package/dist/integrations/jira/integration.d.ts.map +1 -1
  39. package/dist/integrations/jira/integration.js +14 -21
  40. package/dist/integrations/jira/integration.js.map +1 -1
  41. package/dist/integrations/jira/routes.d.ts +3 -0
  42. package/dist/integrations/jira/routes.d.ts.map +1 -1
  43. package/dist/integrations/jira/routes.js +16 -29
  44. package/dist/integrations/jira/routes.js.map +1 -1
  45. package/dist/integrations/jira/rules.d.ts.map +1 -1
  46. package/dist/integrations/jira/rules.js +8 -13
  47. package/dist/integrations/jira/rules.js.map +1 -1
  48. package/dist/integrations/platform/jira/integration.d.ts +7 -1
  49. package/dist/integrations/platform/jira/integration.d.ts.map +1 -1
  50. package/dist/integrations/platform/jira/integration.js +26 -24
  51. package/dist/integrations/platform/jira/integration.js.map +1 -1
  52. package/dist/integrations/platform/jira/routes.d.ts +3 -0
  53. package/dist/integrations/platform/jira/routes.d.ts.map +1 -1
  54. package/dist/integrations/platform/jira/routes.js +17 -28
  55. package/dist/integrations/platform/jira/routes.js.map +1 -1
  56. package/dist/routes/attention-providers.d.ts +2 -1
  57. package/dist/routes/attention-providers.d.ts.map +1 -1
  58. package/dist/routes/attention-providers.js +3 -2
  59. package/dist/routes/attention-providers.js.map +1 -1
  60. package/dist/routes/attention.d.ts +2 -1
  61. package/dist/routes/attention.d.ts.map +1 -1
  62. package/dist/routes/attention.js.map +1 -1
  63. package/dist/routes/boards.d.ts +30 -0
  64. package/dist/routes/boards.d.ts.map +1 -0
  65. package/dist/routes/boards.js +272 -0
  66. package/dist/routes/boards.js.map +1 -0
  67. package/dist/routes/contracts.d.ts +249 -0
  68. package/dist/routes/contracts.d.ts.map +1 -1
  69. package/dist/routes/contracts.js +107 -35
  70. package/dist/routes/contracts.js.map +1 -1
  71. package/dist/routes/intake.d.ts +15 -10
  72. package/dist/routes/intake.d.ts.map +1 -1
  73. package/dist/routes/intake.js +109 -22
  74. package/dist/routes/intake.js.map +1 -1
  75. package/dist/routes/projects.d.ts +3 -0
  76. package/dist/routes/projects.d.ts.map +1 -1
  77. package/dist/routes/projects.js +10 -1
  78. package/dist/routes/projects.js.map +1 -1
  79. package/dist/routes/surface.d.ts +10 -3
  80. package/dist/routes/surface.d.ts.map +1 -1
  81. package/dist/routes/surface.js +12 -1
  82. package/dist/routes/surface.js.map +1 -1
  83. package/dist/routes/tenant-credentials.d.ts +9 -0
  84. package/dist/routes/tenant-credentials.d.ts.map +1 -1
  85. package/dist/routes/tenant-credentials.js +24 -2
  86. package/dist/routes/tenant-credentials.js.map +1 -1
  87. package/dist/routes/work-items.d.ts +3 -1
  88. package/dist/routes/work-items.d.ts.map +1 -1
  89. package/dist/routes/work-items.js +64 -42
  90. package/dist/routes/work-items.js.map +1 -1
  91. package/dist/rules/dispatcher.d.ts +7 -1
  92. package/dist/rules/dispatcher.d.ts.map +1 -1
  93. package/dist/rules/dispatcher.js +70 -34
  94. package/dist/rules/dispatcher.js.map +1 -1
  95. package/dist/rules/processor.d.ts +3 -1
  96. package/dist/rules/processor.d.ts.map +1 -1
  97. package/dist/rules/processor.js +11 -6
  98. package/dist/rules/processor.js.map +1 -1
  99. package/dist/rules/tools.d.ts +2 -1
  100. package/dist/rules/tools.d.ts.map +1 -1
  101. package/dist/rules/tools.js +2 -1
  102. package/dist/rules/tools.js.map +1 -1
  103. package/dist/rules/transition-service.d.ts +3 -1
  104. package/dist/rules/transition-service.d.ts.map +1 -1
  105. package/dist/rules/transition-service.js +12 -12
  106. package/dist/rules/transition-service.js.map +1 -1
  107. package/dist/storage/domains/audit/actions.d.ts +1 -0
  108. package/dist/storage/domains/audit/actions.d.ts.map +1 -1
  109. package/dist/storage/domains/audit/actions.js +6 -1
  110. package/dist/storage/domains/audit/actions.js.map +1 -1
  111. package/dist/storage/domains/boards/base.d.ts +110 -0
  112. package/dist/storage/domains/boards/base.d.ts.map +1 -0
  113. package/dist/storage/domains/boards/base.js +292 -0
  114. package/dist/storage/domains/boards/base.js.map +1 -0
  115. package/dist/storage/domains/boards/schemas.d.ts +11 -0
  116. package/dist/storage/domains/boards/schemas.d.ts.map +1 -0
  117. package/dist/storage/domains/boards/schemas.js +106 -0
  118. package/dist/storage/domains/boards/schemas.js.map +1 -0
  119. package/dist/storage/domains/credentials/base.d.ts +4 -1
  120. package/dist/storage/domains/credentials/base.d.ts.map +1 -1
  121. package/dist/storage/domains/credentials/base.js +5 -2
  122. package/dist/storage/domains/credentials/base.js.map +1 -1
  123. package/dist/storage/domains/intake/base.d.ts +86 -5
  124. package/dist/storage/domains/intake/base.d.ts.map +1 -1
  125. package/dist/storage/domains/intake/base.js +298 -59
  126. package/dist/storage/domains/intake/base.js.map +1 -1
  127. package/dist/storage/domains/projects/base.d.ts +2 -1
  128. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  129. package/dist/storage/domains/projects/base.js +15 -10
  130. package/dist/storage/domains/projects/base.js.map +1 -1
  131. package/dist/storage/domains/work-items/base.d.ts +14 -0
  132. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  133. package/dist/storage/domains/work-items/base.js +39 -11
  134. package/dist/storage/domains/work-items/base.js.map +1 -1
  135. package/dist/tools/read-tools.d.ts +3 -1
  136. package/dist/tools/read-tools.d.ts.map +1 -1
  137. package/dist/tools/read-tools.js +3 -2
  138. package/dist/tools/read-tools.js.map +1 -1
  139. package/dist/tools/session-tools.d.ts +3 -1
  140. package/dist/tools/session-tools.d.ts.map +1 -1
  141. package/dist/tools/session-tools.js +2 -1
  142. package/dist/tools/session-tools.js.map +1 -1
  143. package/dist/tools/sessions-state-processor.d.ts +3 -1
  144. package/dist/tools/sessions-state-processor.d.ts.map +1 -1
  145. package/dist/tools/sessions-state-processor.js +2 -1
  146. package/dist/tools/sessions-state-processor.js.map +1 -1
  147. package/dist/tools/work-item-tools.d.ts +3 -1
  148. package/dist/tools/work-item-tools.d.ts.map +1 -1
  149. package/dist/tools/work-item-tools.js +3 -2
  150. package/dist/tools/work-item-tools.js.map +1 -1
  151. package/dist/workspace.js +1 -1
  152. package/factory-skills/factory-plan/SKILL.md +1 -1
  153. package/factory-skills/factory-triage/SKILL.md +18 -7
  154. package/package.json +9 -9
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { BoardRegistry } from '../boards/index.js';
3
+ import type { ProjectBoardSource } from '../boards/project-boards.js';
3
4
  import type { IntegrationTools } from '../integrations/base.js';
4
5
  import type { AuditStorage } from '../storage/domains/audit/base.js';
5
6
  import type { WorkItemCommentsStorage } from '../storage/domains/comments/base.js';
@@ -13,7 +14,8 @@ export interface FactoryReadToolDependencies {
13
14
  workItems: Pick<WorkItemsStorage, 'list' | 'getForProject' | 'listRunBindings' | 'listDeferredDecisions' | 'listByIds' | 'listDecisionsForEvaluations' | 'listWorkItemSubscribers' | 'listSessionSubscriptions'>;
14
15
  comments: Pick<WorkItemCommentsStorage, 'listRecent'>;
15
16
  audit: Pick<AuditStorage, 'list'>;
16
- boards: BoardRegistry;
17
+ /** A fixed registry, or the per-project boards. */
18
+ boards: BoardRegistry | ProjectBoardSource;
17
19
  /** The live controller, for whether a session is mid-run and when it last moved; absent before the mount is up. */
18
20
  messaging?: () => Promise<Pick<FactoryMessaging, 'controller'> | null>;
19
21
  }
@@ -1 +1 @@
1
- {"version":3,"file":"read-tools.d.ts","sourceRoot":"","sources":["../../src/tools/read-tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAIhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,OAAO,KAAK,EAGV,WAAW,EACX,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAG/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,eAAO,MAAM,QAAQ,MAAM,CAAC;AAC5B,eAAO,MAAM,SAAS,MAAM,CAAC;AAC7B,eAAO,MAAM,QAAQ,KAAK,CAAC;AAG3B,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,MAAM,GACN,eAAe,GACf,iBAAiB,GACjB,uBAAuB,GACvB,WAAW,GACX,6BAA6B,GAC7B,yBAAyB,GACzB,0BAA0B,CAC7B,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,uBAAuB,EAAE,YAAY,CAAC,CAAC;IACtD,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAClC,MAAM,EAAE,aAAa,CAAC;IACtB,mHAAmH;IACnH,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,YAAY,CAAC,GAAG,IAAI,CAAC,CAAC;CACxE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D;AAQD,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,GAAG,MAAM,GAAG,IAAI,CAGjF;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,MAAM,CAEvE;AAED,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,aAOyB,CAAC;AAExD,+GAA+G;AAC/G,wBAAsB,YAAY,CAChC,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,eAAe,CAAC,EAC3D,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,kBAAkB,CAAC,EAC3D,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAQ7B;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAEnD;AA8ED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,2BAA2B,GAAG,gBAAgB,CA6NnH"}
1
+ {"version":3,"file":"read-tools.d.ts","sourceRoot":"","sources":["../../src/tools/read-tools.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAIhE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AACrE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,qCAAqC,CAAC;AACnF,OAAO,KAAK,EAGV,WAAW,EACX,gBAAgB,EACjB,MAAM,uCAAuC,CAAC;AAG/C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,eAAO,MAAM,QAAQ,MAAM,CAAC;AAC5B,eAAO,MAAM,SAAS,MAAM,CAAC;AAC7B,eAAO,MAAM,QAAQ,KAAK,CAAC;AAG3B,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,MAAM,GACN,eAAe,GACf,iBAAiB,GACjB,uBAAuB,GACvB,WAAW,GACX,6BAA6B,GAC7B,yBAAyB,GACzB,0BAA0B,CAC7B,CAAC;IACF,QAAQ,EAAE,IAAI,CAAC,uBAAuB,EAAE,YAAY,CAAC,CAAC;IACtD,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;IAClC,mDAAmD;IACnD,MAAM,EAAE,aAAa,GAAG,kBAAkB,CAAC;IAC3C,mHAAmH;IACnH,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,YAAY,CAAC,GAAG,IAAI,CAAC,CAAC;CACxE;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE1D;AAQD,4FAA4F;AAC5F,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,GAAG,MAAM,GAAG,IAAI,CAGjF;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,GAAG,MAAM,CAEvE;AAED,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,aAOyB,CAAC;AAExD,+GAA+G;AAC/G,wBAAsB,YAAY,CAChC,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,eAAe,CAAC,EAC3D,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,GAAG,kBAAkB,CAAC,EAC3D,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAQ7B;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAEnD;AA8ED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,gBAAgB,EAAE,IAAI,EAAE,2BAA2B,GAAG,gBAAgB,CA6NnH"}
@@ -1,5 +1,6 @@
1
1
  import { boardForWorkItem } from "../boards/semantics.js";
2
2
  import "../boards/index.js";
3
+ import { toBoardSource } from "../boards/project-boards.js";
3
4
  import { factoryDispatchFailureMetadata } from "../rules/dispatch-errors.js";
4
5
  import { sessionActivity } from "./deliver.js";
5
6
  import { factorySessionUrl, factoryWorkItemUrl } from "../links.js";
@@ -176,7 +177,7 @@ function createFactoryReadTools(scope, deps) {
176
177
  })
177
178
  ]);
178
179
  const itemDecisions = decisions.slice(-50);
179
- const overtaken = await overtakenDecisionIds(deps.workItems, deps.boards, project, itemDecisions.filter((decision) => decision.status === "failed"));
180
+ const overtaken = await overtakenDecisionIds(deps.workItems, await toBoardSource(deps.boards).load(project), project, itemDecisions.filter((decision) => decision.status === "failed"));
180
181
  const labels = Array.isArray(item.metadata?.labels) ? item.metadata.labels.filter((label) => typeof label === "string").slice(0, 50) : [];
181
182
  const parent = item.parentWorkItemId ? all.find((candidate) => candidate.id === item.parentWorkItemId) : void 0;
182
183
  const children = all.filter((candidate) => candidate.parentWorkItemId === item.id);
@@ -272,7 +273,7 @@ function createFactoryReadTools(scope, deps) {
272
273
  ...group,
273
274
  shown: group.decisions.slice(0, limit)
274
275
  }));
275
- const overtaken = await overtakenDecisionIds(deps.workItems, deps.boards, project, incidents.flatMap((group) => group.shown));
276
+ const overtaken = await overtakenDecisionIds(deps.workItems, await toBoardSource(deps.boards).load(project), project, incidents.flatMap((group) => group.shown));
276
277
  const proposals = decisions.filter((decision) => decision.status === "proposed");
277
278
  const held = items.filter(isHeld);
278
279
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"read-tools.js","names":[],"sources":["../../src/tools/read-tools.ts"],"sourcesContent":["import { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { factorySessionUrl, factoryWorkItemUrl } from '../links.js';\nimport { decisionRetryable, overtakenDecisionIds } from '../rules/decision-applicability.js';\nimport { factoryDispatchFailureMetadata } from '../rules/dispatch-errors.js';\nimport type { AuditStorage } from '../storage/domains/audit/base.js';\nimport type { WorkItemCommentsStorage } from '../storage/domains/comments/base.js';\nimport type {\n FactoryDeferredDecisionRecord,\n FactoryRunBindingRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport { sessionActivity } from './deliver.js';\nimport type { FactorySessionActivity } from './deliver.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport type { FactoryToolScope } from './scope.js';\n\nexport const MAX_TEXT = 600;\nexport const MAX_ERROR = 400;\nexport const MAX_LIST = 50;\nconst DEFAULT_LIST = 25;\n\nexport interface FactoryReadToolDependencies {\n workItems: Pick<\n WorkItemsStorage,\n | 'list'\n | 'getForProject'\n | 'listRunBindings'\n | 'listDeferredDecisions'\n | 'listByIds'\n | 'listDecisionsForEvaluations'\n | 'listWorkItemSubscribers'\n | 'listSessionSubscriptions'\n >;\n comments: Pick<WorkItemCommentsStorage, 'listRecent'>;\n audit: Pick<AuditStorage, 'list'>;\n boards: BoardRegistry;\n /** The live controller, for whether a session is mid-run and when it last moved; absent before the mount is up. */\n messaging?: () => Promise<Pick<FactoryMessaging, 'controller'> | null>;\n}\n\nexport function truncate(text: string, max: number): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\nfunction iso(value: Date | string | null | undefined): string | null {\n if (!value) return null;\n const date = value instanceof Date ? value : new Date(value);\n return Number.isFinite(date.getTime()) ? date.toISOString() : null;\n}\n\n/** The number a person sees on the card: the issue or PR number the item was filed from. */\nexport function workItemNumber(item: Pick<WorkItemRow, 'metadata'>): number | null {\n const number = item.metadata?.number ?? item.metadata?.githubIssueNumber ?? item.metadata?.githubPullRequestNumber;\n return typeof number === 'number' ? number : null;\n}\n\nexport function workItemStage(item: Pick<WorkItemRow, 'stages'>): string {\n return item.stages.join('+');\n}\n\n/** A work item id or its card number; anything else can't name an item. */\nexport const workItemRefSchema = z\n .string()\n .trim()\n .max(64)\n .refine(ref => z.string().uuid().safeParse(ref).success || /^#?\\d+$/.test(ref), {\n message: 'Use a work item id (UUID) or its number like \"#123\".',\n })\n .describe('Work item id, or its number like \"#123\".');\n\n/** Resolve `<uuid>` or `#123` to an item in this Factory only. Pass a ref validated by `workItemRefSchema`. */\nexport async function findWorkItem(\n workItems: Pick<WorkItemsStorage, 'list' | 'getForProject'>,\n scope: Pick<FactoryToolScope, 'orgId' | 'factoryProjectId'>,\n ref: string,\n): Promise<WorkItemRow | null> {\n const trimmed = ref.trim();\n const number = /^#?(\\d+)$/.exec(trimmed)?.[1];\n if (number === undefined) return workItems.getForProject(scope.orgId, scope.factoryProjectId, trimmed);\n const items = await workItems.list(scope);\n const matches = items.filter(item => workItemNumber(item) === Number(number));\n // Issue and PR numbers share a space on GitHub; prefer the Work-board card.\n return matches.find(item => item.externalSource?.type !== 'pull-request') ?? matches[0] ?? null;\n}\n\nexport function workItemNotFound(ref: string): Error {\n return new Error(`No work item ${ref} was found in this Factory.`);\n}\n\nfunction activeWorker(bindings: FactoryRunBindingRecord[], itemId: string) {\n const binding = bindings.find(candidate => candidate.workItemId === itemId && candidate.status === 'active');\n return binding ? { role: binding.role, sessionId: binding.sessionId } : null;\n}\n\n/** The card on its Factory board; null when the server has no public URL configured. */\nfunction workItemUrl(item: WorkItemRow) {\n return (\n factoryWorkItemUrl({\n factoryProjectId: item.factoryProjectId,\n board: boardForWorkItem(item),\n workItemId: item.id,\n }) ?? null\n );\n}\n\nfunction summarizeItem(item: WorkItemRow, bindings: FactoryRunBindingRecord[]) {\n return {\n id: item.id,\n number: workItemNumber(item),\n title: truncate(item.title, MAX_TEXT),\n board: boardForWorkItem(item),\n stage: workItemStage(item),\n url: workItemUrl(item),\n activeWorker: activeWorker(bindings, item.id),\n requestedBy: item.createdBy,\n updatedAt: iso(item.updatedAt),\n };\n}\n\nfunction summarizeDecision(decision: FactoryDeferredDecisionRecord, overtaken = false) {\n const type = typeof decision.decision.type === 'string' ? decision.decision.type : 'decision';\n const role = typeof decision.decision.role === 'string' ? decision.decision.role : null;\n return {\n id: decision.id,\n workItemId: decision.workItemId,\n type,\n role,\n status: decision.status,\n attempts: decision.attempts,\n failureCode: decision.failureCode,\n failureLabel: decision.failureCode ? factoryDispatchFailureMetadata(decision.failureCode).label : null,\n canRetry: decisionRetryable(decision, overtaken),\n error: decision.lastError ? truncate(decision.lastError, MAX_ERROR) : null,\n approvedBy: decision.approvedBy,\n updatedAt: iso(decision.updatedAt),\n };\n}\n\n/** Held: a non-bug item triage parked until a person accepts it. */\nfunction isHeld(item: WorkItemRow): boolean {\n return item.stages.includes('triage') && !!item.triageType && item.triageType !== 'bug' && !item.acceptedAt;\n}\n\nconst listInputSchema = z\n .object({\n query: z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe('Case-insensitive text in the title, or an exact card number like \"#123\".'),\n stage: z.string().min(1).max(64).optional().describe('Only items in this stage, e.g. \"triage\" or \"review\".'),\n board: z.string().min(1).max(64).optional().describe('Only items on this board, e.g. \"work\" or \"review\".'),\n role: z.string().min(1).max(64).optional().describe('Only items that have a session for this role.'),\n active: z.boolean().optional().describe('true: only items with an agent working now; false: only idle items.'),\n requestedBy: z.literal('me').optional().describe('\"me\": only items you filed.'),\n subscribed: z\n .boolean()\n .optional()\n .describe('true: only items this session follows; false: only items it does not.'),\n limit: z.number().int().min(1).max(MAX_LIST).optional().describe(`Default ${DEFAULT_LIST}, max ${MAX_LIST}.`),\n })\n .strict();\n\nexport function createFactoryReadTools(scope: FactoryToolScope, deps: FactoryReadToolDependencies): IntegrationTools {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n\n return {\n factory_list_work_items: createTool({\n id: 'factory_list_work_items',\n description:\n \"List this Factory's work items, newest activity first, with counts per stage for the whole Factory. Filter by text or #number, stage, board, role, whether an agent is working on it, items you filed, or items this session follows. Check here for an existing item before filing a new one.\",\n inputSchema: listInputSchema,\n execute: async input => {\n const [items, bindings, followed] = await Promise.all([\n deps.workItems.list(project),\n deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId),\n input.subscribed === undefined\n ? undefined\n : deps.workItems.listSessionSubscriptions({ ...project, sessionId: scope.sessionId }),\n ]);\n const followedIds = new Set(followed?.map(subscription => subscription.workItemId));\n const byStage: Record<string, number> = {};\n for (const item of items) {\n const stage = workItemStage(item);\n byStage[stage] = (byStage[stage] ?? 0) + 1;\n }\n const numberQuery = input.query ? /^#(\\d+)$/.exec(input.query)?.[1] : undefined;\n const text = input.query?.toLowerCase();\n const matches = items\n .filter(item => {\n if (numberQuery !== undefined) {\n if (workItemNumber(item) !== Number(numberQuery)) return false;\n } else if (text && !item.title.toLowerCase().includes(text)) return false;\n if (input.stage && !item.stages.includes(input.stage)) return false;\n if (input.board && boardForWorkItem(item) !== input.board) return false;\n if (input.role && !item.sessions[input.role]) return false;\n if (input.active !== undefined && (activeWorker(bindings, item.id) !== null) !== input.active) return false;\n if (input.requestedBy === 'me' && item.createdBy !== scope.userId) return false;\n if (input.subscribed !== undefined && followedIds.has(item.id) !== input.subscribed) return false;\n return true;\n })\n .sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime());\n const limit = input.limit ?? DEFAULT_LIST;\n return {\n counts: { total: items.length, byStage },\n items: matches.slice(0, limit).map(item => summarizeItem(item, bindings)),\n truncated: matches.length > limit,\n };\n },\n }),\n\n factory_inspect_work_item: createTool({\n id: 'factory_inspect_work_item',\n description:\n 'Everything the Factory knows about one work item: stage history, sessions by role (with whether each is mid-run and when its thread last moved), decisions with errors, recent audit events, recent feed comments, labels, linked items and the sessions following it. Use it to explain why an item is where it is.',\n inputSchema: z.object({ workItem: workItemRefSchema }).strict(),\n execute: async ({ workItem }) => {\n const item = await findWorkItem(deps.workItems, scope, workItem);\n if (!item) throw workItemNotFound(workItem);\n const [bindings, decisions, audit, feed, all, subscribers] = await Promise.all([\n deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id),\n deps.workItems.listDeferredDecisions(scope.orgId, scope.factoryProjectId, { workItemId: item.id }),\n deps.audit.list({ ...project, limit: 200 }),\n deps.comments.listRecent({ ...project, workItemId: item.id, limit: 10 }),\n deps.workItems.list(project),\n deps.workItems.listWorkItemSubscribers({ orgId: scope.orgId, workItemId: item.id }),\n ]);\n const itemDecisions = decisions.slice(-MAX_LIST);\n const overtaken = await overtakenDecisionIds(\n deps.workItems,\n deps.boards,\n project,\n itemDecisions.filter(decision => decision.status === 'failed'),\n );\n const labels = Array.isArray(item.metadata?.labels)\n ? item.metadata.labels.filter((label): label is string => typeof label === 'string').slice(0, MAX_LIST)\n : [];\n const parent = item.parentWorkItemId\n ? all.find(candidate => candidate.id === item.parentWorkItemId)\n : undefined;\n const children = all.filter(candidate => candidate.parentWorkItemId === item.id);\n const controller = (await deps.messaging?.())?.controller;\n const activity = async (sessionId: string, threadId: string): Promise<FactorySessionActivity | null> =>\n controller ? sessionActivity(controller, sessionId, threadId) : null;\n const sessions = await Promise.all(\n Object.entries(item.sessions).map(async ([role, ref]) => ({\n role,\n sessionId: ref.sessionId,\n threadId: ref.threadId,\n url:\n factorySessionUrl({\n factoryProjectId: item.factoryProjectId,\n sessionId: ref.sessionId,\n threadId: ref.threadId,\n }) ?? null,\n startedBy: ref.startedBy,\n active: bindings.some(\n binding => binding.role === role && binding.sessionId === ref.sessionId && binding.status === 'active',\n ),\n // `running` with an old `lastActivityAt` is a stuck run; `running` with a recent one is a long one.\n ...((await activity(ref.sessionId, ref.threadId)) ?? { running: null, lastActivityAt: null }),\n })),\n );\n return {\n item: {\n ...summarizeItem(item, bindings),\n source: item.externalSource ? `${item.externalSource.integrationId}:${item.externalSource.type}` : 'manual',\n sourceUrl: typeof item.metadata?.url === 'string' ? item.metadata.url : null,\n triageType: item.triageType,\n revision: item.revision,\n },\n labels,\n stageHistory: item.stageHistory.slice(-MAX_LIST).map(entry => ({\n stage: entry.stage,\n enteredAt: entry.enteredAt,\n exitedAt: entry.exitedAt ?? null,\n by: entry.by,\n })),\n sessions,\n decisions: itemDecisions.map(decision => summarizeDecision(decision, overtaken.has(decision.id))),\n audit: audit.events\n .filter(event => event.targets.some(target => target.type === 'work_item' && target.id === item.id))\n .slice(0, 25)\n .map(event => ({\n action: event.action,\n actorId: event.actorId,\n actorType: event.actorType,\n occurredAt: iso(event.occurredAt),\n })),\n feed: feed.map(comment => ({\n id: comment.id,\n kind: comment.kind,\n author: comment.author,\n occurredAt: iso(comment.occurredAt),\n body: truncate(comment.body, MAX_TEXT),\n })),\n parent: parent ? summarizeItem(parent, bindings) : null,\n children: children.slice(0, MAX_LIST).map(child => summarizeItem(child, bindings)),\n subscribers: subscribers.slice(0, MAX_LIST).map(subscription => ({\n sessionId: subscription.sessionId,\n reason: subscription.reason,\n isCaller: subscription.sessionId === scope.sessionId,\n })),\n };\n },\n }),\n\n factory_list_attention: createTool({\n id: 'factory_list_attention',\n description:\n 'What in this Factory needs a person: failed decisions grouped by failure code and error (one incident per group), proposals waiting for approval, and items triage is holding until someone accepts them.',\n inputSchema: z.object({ limit: z.number().int().min(1).max(MAX_LIST).optional() }).strict(),\n execute: async ({ limit = DEFAULT_LIST }) => {\n const [decisions, items] = await Promise.all([\n deps.workItems.listDeferredDecisions(scope.orgId, scope.factoryProjectId, {\n statuses: ['failed', 'proposed'],\n }),\n deps.workItems.list(project),\n ]);\n const itemsById = new Map(items.map(item => [item.id, item]));\n const failed = decisions.filter(decision => decision.status === 'failed');\n const groups = new Map<\n string,\n { failureCode: string; error: string; decisions: FactoryDeferredDecisionRecord[] }\n >();\n for (const decision of failed) {\n const error = truncate(decision.lastError ?? '', MAX_ERROR);\n const key = `${decision.failureCode ?? 'unknown'}|${error}`;\n const group = groups.get(key) ?? { failureCode: decision.failureCode ?? 'unknown', error, decisions: [] };\n group.decisions.push(decision);\n groups.set(key, group);\n }\n const ref = (workItemId: string | null) => {\n const item = workItemId ? itemsById.get(workItemId) : undefined;\n return {\n workItemId,\n number: item ? workItemNumber(item) : null,\n title: item ? truncate(item.title, MAX_TEXT) : null,\n url: item ? workItemUrl(item) : null,\n };\n };\n const allIncidents = [...groups.values()].sort((a, b) => b.decisions.length - a.decisions.length);\n const incidents = allIncidents\n .slice(0, limit)\n .map(group => ({ ...group, shown: group.decisions.slice(0, limit) }));\n // Check exactly the decisions this call returns, so every canRetry is real.\n const overtaken = await overtakenDecisionIds(\n deps.workItems,\n deps.boards,\n project,\n incidents.flatMap(group => group.shown),\n );\n const proposals = decisions.filter(decision => decision.status === 'proposed');\n const held = items.filter(isHeld);\n return {\n incidents: incidents.map(group => ({\n failureCode: group.failureCode,\n failureLabel: factoryDispatchFailureMetadata(group.decisions[0]!.failureCode).label,\n error: group.error,\n count: group.decisions.length,\n decisions: group.shown.map(decision => ({\n id: decision.id,\n ...ref(decision.workItemId),\n attempts: decision.attempts,\n canRetry: decisionRetryable(decision, overtaken.has(decision.id)),\n })),\n })),\n proposals: proposals\n .slice(0, limit)\n .map(decision => ({ ...summarizeDecision(decision), ...ref(decision.workItemId) })),\n held: held.slice(0, limit).map(item => ({\n ...ref(item.id),\n triageType: item.triageType,\n stage: workItemStage(item),\n })),\n truncated:\n allIncidents.length > limit ||\n incidents.some(group => group.shown.length < group.decisions.length) ||\n proposals.length > limit ||\n held.length > limit,\n };\n },\n }),\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"read-tools.js","names":[],"sources":["../../src/tools/read-tools.ts"],"sourcesContent":["import { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport { toBoardSource } from '../boards/project-boards.js';\nimport type { ProjectBoardSource } from '../boards/project-boards.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { factorySessionUrl, factoryWorkItemUrl } from '../links.js';\nimport { decisionRetryable, overtakenDecisionIds } from '../rules/decision-applicability.js';\nimport { factoryDispatchFailureMetadata } from '../rules/dispatch-errors.js';\nimport type { AuditStorage } from '../storage/domains/audit/base.js';\nimport type { WorkItemCommentsStorage } from '../storage/domains/comments/base.js';\nimport type {\n FactoryDeferredDecisionRecord,\n FactoryRunBindingRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport { sessionActivity } from './deliver.js';\nimport type { FactorySessionActivity } from './deliver.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport type { FactoryToolScope } from './scope.js';\n\nexport const MAX_TEXT = 600;\nexport const MAX_ERROR = 400;\nexport const MAX_LIST = 50;\nconst DEFAULT_LIST = 25;\n\nexport interface FactoryReadToolDependencies {\n workItems: Pick<\n WorkItemsStorage,\n | 'list'\n | 'getForProject'\n | 'listRunBindings'\n | 'listDeferredDecisions'\n | 'listByIds'\n | 'listDecisionsForEvaluations'\n | 'listWorkItemSubscribers'\n | 'listSessionSubscriptions'\n >;\n comments: Pick<WorkItemCommentsStorage, 'listRecent'>;\n audit: Pick<AuditStorage, 'list'>;\n /** A fixed registry, or the per-project boards. */\n boards: BoardRegistry | ProjectBoardSource;\n /** The live controller, for whether a session is mid-run and when it last moved; absent before the mount is up. */\n messaging?: () => Promise<Pick<FactoryMessaging, 'controller'> | null>;\n}\n\nexport function truncate(text: string, max: number): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\nfunction iso(value: Date | string | null | undefined): string | null {\n if (!value) return null;\n const date = value instanceof Date ? value : new Date(value);\n return Number.isFinite(date.getTime()) ? date.toISOString() : null;\n}\n\n/** The number a person sees on the card: the issue or PR number the item was filed from. */\nexport function workItemNumber(item: Pick<WorkItemRow, 'metadata'>): number | null {\n const number = item.metadata?.number ?? item.metadata?.githubIssueNumber ?? item.metadata?.githubPullRequestNumber;\n return typeof number === 'number' ? number : null;\n}\n\nexport function workItemStage(item: Pick<WorkItemRow, 'stages'>): string {\n return item.stages.join('+');\n}\n\n/** A work item id or its card number; anything else can't name an item. */\nexport const workItemRefSchema = z\n .string()\n .trim()\n .max(64)\n .refine(ref => z.string().uuid().safeParse(ref).success || /^#?\\d+$/.test(ref), {\n message: 'Use a work item id (UUID) or its number like \"#123\".',\n })\n .describe('Work item id, or its number like \"#123\".');\n\n/** Resolve `<uuid>` or `#123` to an item in this Factory only. Pass a ref validated by `workItemRefSchema`. */\nexport async function findWorkItem(\n workItems: Pick<WorkItemsStorage, 'list' | 'getForProject'>,\n scope: Pick<FactoryToolScope, 'orgId' | 'factoryProjectId'>,\n ref: string,\n): Promise<WorkItemRow | null> {\n const trimmed = ref.trim();\n const number = /^#?(\\d+)$/.exec(trimmed)?.[1];\n if (number === undefined) return workItems.getForProject(scope.orgId, scope.factoryProjectId, trimmed);\n const items = await workItems.list(scope);\n const matches = items.filter(item => workItemNumber(item) === Number(number));\n // Issue and PR numbers share a space on GitHub; prefer the Work-board card.\n return matches.find(item => item.externalSource?.type !== 'pull-request') ?? matches[0] ?? null;\n}\n\nexport function workItemNotFound(ref: string): Error {\n return new Error(`No work item ${ref} was found in this Factory.`);\n}\n\nfunction activeWorker(bindings: FactoryRunBindingRecord[], itemId: string) {\n const binding = bindings.find(candidate => candidate.workItemId === itemId && candidate.status === 'active');\n return binding ? { role: binding.role, sessionId: binding.sessionId } : null;\n}\n\n/** The card on its Factory board; null when the server has no public URL configured. */\nfunction workItemUrl(item: WorkItemRow) {\n return (\n factoryWorkItemUrl({\n factoryProjectId: item.factoryProjectId,\n board: boardForWorkItem(item),\n workItemId: item.id,\n }) ?? null\n );\n}\n\nfunction summarizeItem(item: WorkItemRow, bindings: FactoryRunBindingRecord[]) {\n return {\n id: item.id,\n number: workItemNumber(item),\n title: truncate(item.title, MAX_TEXT),\n board: boardForWorkItem(item),\n stage: workItemStage(item),\n url: workItemUrl(item),\n activeWorker: activeWorker(bindings, item.id),\n requestedBy: item.createdBy,\n updatedAt: iso(item.updatedAt),\n };\n}\n\nfunction summarizeDecision(decision: FactoryDeferredDecisionRecord, overtaken = false) {\n const type = typeof decision.decision.type === 'string' ? decision.decision.type : 'decision';\n const role = typeof decision.decision.role === 'string' ? decision.decision.role : null;\n return {\n id: decision.id,\n workItemId: decision.workItemId,\n type,\n role,\n status: decision.status,\n attempts: decision.attempts,\n failureCode: decision.failureCode,\n failureLabel: decision.failureCode ? factoryDispatchFailureMetadata(decision.failureCode).label : null,\n canRetry: decisionRetryable(decision, overtaken),\n error: decision.lastError ? truncate(decision.lastError, MAX_ERROR) : null,\n approvedBy: decision.approvedBy,\n updatedAt: iso(decision.updatedAt),\n };\n}\n\n/** Held: a non-bug item triage parked until a person accepts it. */\nfunction isHeld(item: WorkItemRow): boolean {\n return item.stages.includes('triage') && !!item.triageType && item.triageType !== 'bug' && !item.acceptedAt;\n}\n\nconst listInputSchema = z\n .object({\n query: z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe('Case-insensitive text in the title, or an exact card number like \"#123\".'),\n stage: z.string().min(1).max(64).optional().describe('Only items in this stage, e.g. \"triage\" or \"review\".'),\n board: z.string().min(1).max(64).optional().describe('Only items on this board, e.g. \"work\" or \"review\".'),\n role: z.string().min(1).max(64).optional().describe('Only items that have a session for this role.'),\n active: z.boolean().optional().describe('true: only items with an agent working now; false: only idle items.'),\n requestedBy: z.literal('me').optional().describe('\"me\": only items you filed.'),\n subscribed: z\n .boolean()\n .optional()\n .describe('true: only items this session follows; false: only items it does not.'),\n limit: z.number().int().min(1).max(MAX_LIST).optional().describe(`Default ${DEFAULT_LIST}, max ${MAX_LIST}.`),\n })\n .strict();\n\nexport function createFactoryReadTools(scope: FactoryToolScope, deps: FactoryReadToolDependencies): IntegrationTools {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n\n return {\n factory_list_work_items: createTool({\n id: 'factory_list_work_items',\n description:\n \"List this Factory's work items, newest activity first, with counts per stage for the whole Factory. Filter by text or #number, stage, board, role, whether an agent is working on it, items you filed, or items this session follows. Check here for an existing item before filing a new one.\",\n inputSchema: listInputSchema,\n execute: async input => {\n const [items, bindings, followed] = await Promise.all([\n deps.workItems.list(project),\n deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId),\n input.subscribed === undefined\n ? undefined\n : deps.workItems.listSessionSubscriptions({ ...project, sessionId: scope.sessionId }),\n ]);\n const followedIds = new Set(followed?.map(subscription => subscription.workItemId));\n const byStage: Record<string, number> = {};\n for (const item of items) {\n const stage = workItemStage(item);\n byStage[stage] = (byStage[stage] ?? 0) + 1;\n }\n const numberQuery = input.query ? /^#(\\d+)$/.exec(input.query)?.[1] : undefined;\n const text = input.query?.toLowerCase();\n const matches = items\n .filter(item => {\n if (numberQuery !== undefined) {\n if (workItemNumber(item) !== Number(numberQuery)) return false;\n } else if (text && !item.title.toLowerCase().includes(text)) return false;\n if (input.stage && !item.stages.includes(input.stage)) return false;\n if (input.board && boardForWorkItem(item) !== input.board) return false;\n if (input.role && !item.sessions[input.role]) return false;\n if (input.active !== undefined && (activeWorker(bindings, item.id) !== null) !== input.active) return false;\n if (input.requestedBy === 'me' && item.createdBy !== scope.userId) return false;\n if (input.subscribed !== undefined && followedIds.has(item.id) !== input.subscribed) return false;\n return true;\n })\n .sort((left, right) => right.updatedAt.getTime() - left.updatedAt.getTime());\n const limit = input.limit ?? DEFAULT_LIST;\n return {\n counts: { total: items.length, byStage },\n items: matches.slice(0, limit).map(item => summarizeItem(item, bindings)),\n truncated: matches.length > limit,\n };\n },\n }),\n\n factory_inspect_work_item: createTool({\n id: 'factory_inspect_work_item',\n description:\n 'Everything the Factory knows about one work item: stage history, sessions by role (with whether each is mid-run and when its thread last moved), decisions with errors, recent audit events, recent feed comments, labels, linked items and the sessions following it. Use it to explain why an item is where it is.',\n inputSchema: z.object({ workItem: workItemRefSchema }).strict(),\n execute: async ({ workItem }) => {\n const item = await findWorkItem(deps.workItems, scope, workItem);\n if (!item) throw workItemNotFound(workItem);\n const [bindings, decisions, audit, feed, all, subscribers] = await Promise.all([\n deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id),\n deps.workItems.listDeferredDecisions(scope.orgId, scope.factoryProjectId, { workItemId: item.id }),\n deps.audit.list({ ...project, limit: 200 }),\n deps.comments.listRecent({ ...project, workItemId: item.id, limit: 10 }),\n deps.workItems.list(project),\n deps.workItems.listWorkItemSubscribers({ orgId: scope.orgId, workItemId: item.id }),\n ]);\n const itemDecisions = decisions.slice(-MAX_LIST);\n const overtaken = await overtakenDecisionIds(\n deps.workItems,\n await toBoardSource(deps.boards).load(project),\n project,\n itemDecisions.filter(decision => decision.status === 'failed'),\n );\n const labels = Array.isArray(item.metadata?.labels)\n ? item.metadata.labels.filter((label): label is string => typeof label === 'string').slice(0, MAX_LIST)\n : [];\n const parent = item.parentWorkItemId\n ? all.find(candidate => candidate.id === item.parentWorkItemId)\n : undefined;\n const children = all.filter(candidate => candidate.parentWorkItemId === item.id);\n const controller = (await deps.messaging?.())?.controller;\n const activity = async (sessionId: string, threadId: string): Promise<FactorySessionActivity | null> =>\n controller ? sessionActivity(controller, sessionId, threadId) : null;\n const sessions = await Promise.all(\n Object.entries(item.sessions).map(async ([role, ref]) => ({\n role,\n sessionId: ref.sessionId,\n threadId: ref.threadId,\n url:\n factorySessionUrl({\n factoryProjectId: item.factoryProjectId,\n sessionId: ref.sessionId,\n threadId: ref.threadId,\n }) ?? null,\n startedBy: ref.startedBy,\n active: bindings.some(\n binding => binding.role === role && binding.sessionId === ref.sessionId && binding.status === 'active',\n ),\n // `running` with an old `lastActivityAt` is a stuck run; `running` with a recent one is a long one.\n ...((await activity(ref.sessionId, ref.threadId)) ?? { running: null, lastActivityAt: null }),\n })),\n );\n return {\n item: {\n ...summarizeItem(item, bindings),\n source: item.externalSource ? `${item.externalSource.integrationId}:${item.externalSource.type}` : 'manual',\n sourceUrl: typeof item.metadata?.url === 'string' ? item.metadata.url : null,\n triageType: item.triageType,\n revision: item.revision,\n },\n labels,\n stageHistory: item.stageHistory.slice(-MAX_LIST).map(entry => ({\n stage: entry.stage,\n enteredAt: entry.enteredAt,\n exitedAt: entry.exitedAt ?? null,\n by: entry.by,\n })),\n sessions,\n decisions: itemDecisions.map(decision => summarizeDecision(decision, overtaken.has(decision.id))),\n audit: audit.events\n .filter(event => event.targets.some(target => target.type === 'work_item' && target.id === item.id))\n .slice(0, 25)\n .map(event => ({\n action: event.action,\n actorId: event.actorId,\n actorType: event.actorType,\n occurredAt: iso(event.occurredAt),\n })),\n feed: feed.map(comment => ({\n id: comment.id,\n kind: comment.kind,\n author: comment.author,\n occurredAt: iso(comment.occurredAt),\n body: truncate(comment.body, MAX_TEXT),\n })),\n parent: parent ? summarizeItem(parent, bindings) : null,\n children: children.slice(0, MAX_LIST).map(child => summarizeItem(child, bindings)),\n subscribers: subscribers.slice(0, MAX_LIST).map(subscription => ({\n sessionId: subscription.sessionId,\n reason: subscription.reason,\n isCaller: subscription.sessionId === scope.sessionId,\n })),\n };\n },\n }),\n\n factory_list_attention: createTool({\n id: 'factory_list_attention',\n description:\n 'What in this Factory needs a person: failed decisions grouped by failure code and error (one incident per group), proposals waiting for approval, and items triage is holding until someone accepts them.',\n inputSchema: z.object({ limit: z.number().int().min(1).max(MAX_LIST).optional() }).strict(),\n execute: async ({ limit = DEFAULT_LIST }) => {\n const [decisions, items] = await Promise.all([\n deps.workItems.listDeferredDecisions(scope.orgId, scope.factoryProjectId, {\n statuses: ['failed', 'proposed'],\n }),\n deps.workItems.list(project),\n ]);\n const itemsById = new Map(items.map(item => [item.id, item]));\n const failed = decisions.filter(decision => decision.status === 'failed');\n const groups = new Map<\n string,\n { failureCode: string; error: string; decisions: FactoryDeferredDecisionRecord[] }\n >();\n for (const decision of failed) {\n const error = truncate(decision.lastError ?? '', MAX_ERROR);\n const key = `${decision.failureCode ?? 'unknown'}|${error}`;\n const group = groups.get(key) ?? { failureCode: decision.failureCode ?? 'unknown', error, decisions: [] };\n group.decisions.push(decision);\n groups.set(key, group);\n }\n const ref = (workItemId: string | null) => {\n const item = workItemId ? itemsById.get(workItemId) : undefined;\n return {\n workItemId,\n number: item ? workItemNumber(item) : null,\n title: item ? truncate(item.title, MAX_TEXT) : null,\n url: item ? workItemUrl(item) : null,\n };\n };\n const allIncidents = [...groups.values()].sort((a, b) => b.decisions.length - a.decisions.length);\n const incidents = allIncidents\n .slice(0, limit)\n .map(group => ({ ...group, shown: group.decisions.slice(0, limit) }));\n // Check exactly the decisions this call returns, so every canRetry is real.\n const overtaken = await overtakenDecisionIds(\n deps.workItems,\n await toBoardSource(deps.boards).load(project),\n project,\n incidents.flatMap(group => group.shown),\n );\n const proposals = decisions.filter(decision => decision.status === 'proposed');\n const held = items.filter(isHeld);\n return {\n incidents: incidents.map(group => ({\n failureCode: group.failureCode,\n failureLabel: factoryDispatchFailureMetadata(group.decisions[0]!.failureCode).label,\n error: group.error,\n count: group.decisions.length,\n decisions: group.shown.map(decision => ({\n id: decision.id,\n ...ref(decision.workItemId),\n attempts: decision.attempts,\n canRetry: decisionRetryable(decision, overtaken.has(decision.id)),\n })),\n })),\n proposals: proposals\n .slice(0, limit)\n .map(decision => ({ ...summarizeDecision(decision), ...ref(decision.workItemId) })),\n held: held.slice(0, limit).map(item => ({\n ...ref(item.id),\n triageType: item.triageType,\n stage: workItemStage(item),\n })),\n truncated:\n allIncidents.length > limit ||\n incidents.some(group => group.shown.length < group.decisions.length) ||\n proposals.length > limit ||\n held.length > limit,\n };\n },\n }),\n };\n}\n"],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import type { ProjectBoardSource } from '../boards/project-boards.js';
1
2
  import type { BoardRegistry } from '../boards/registry.js';
2
3
  import type { IntegrationTools } from '../integrations/base.js';
3
4
  import type { FactoryStartCoordinator } from '../rules/start-coordinator.js';
@@ -32,7 +33,8 @@ export interface FactorySessionToolDependencies {
32
33
  /** Resolved lazily: the controller only exists once the mount is up. */
33
34
  messaging: () => Promise<FactoryMessaging | null>;
34
35
  workItems: Pick<WorkItemsStorage, 'list' | 'getForProject' | 'listRunBindings' | 'findActiveRunBindingForSession' | 'findRunBindingByThread' | 'hasRunBindingForSession' | 'subscribeToWorkItem' | 'unsubscribeFromWorkItem' | 'listSessionSubscriptions' | 'listWorkItemSubscribers'>;
35
- boards: BoardRegistry;
36
+ /** A fixed registry, or the per-project boards. */
37
+ boards: BoardRegistry | ProjectBoardSource;
36
38
  auditRecorder: AuditRecorder;
37
39
  intake?: Pick<IntakeStorage, 'getConfig'>;
38
40
  /** The browser Start's coordinator; absent until the Factory runtime is ready. */
@@ -1 +1 @@
1
- {"version":3,"file":"session-tools.d.ts","sourceRoot":"","sources":["../../src/tools/session-tools.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAMhE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE7E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAQhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAElF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnD,sGAAsG;AACtG,eAAO,MAAM,sBAAsB,QAAoD,CAAC;AAExF,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,6BAA6B,CAAC,CAAC;IAC7F,YAAY,EAAE;QACZ,QAAQ,EAAE,sBAAsB,CAAC;QACjC,aAAa,EAAE,oBAAoB,CAAC;QACpC;;;;WAIG;QACH,aAAa,CAAC,EAAE;YACd,WAAW,EAAE,IAAI,CAAC,uBAAuB,EAAE,iBAAiB,CAAC,CAAC;YAC9D,eAAe,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,aAAa,GAAG,MAAM,CAAC,CAAC;YACvE,mBAAmB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;SAC3C,CAAC;KACH,CAAC;IACF,wEAAwE;IACxE,SAAS,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAClD,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,MAAM,GACN,eAAe,GACf,iBAAiB,GACjB,gCAAgC,GAChC,wBAAwB,GACxB,yBAAyB,GACzB,qBAAqB,GACrB,yBAAyB,GACzB,0BAA0B,GAC1B,yBAAyB,CAC5B,CAAC;IACF,MAAM,EAAE,aAAa,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IAC1C,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC;CAC9E;AA0BD,oGAAoG;AACpG,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,8BAA8B,GACnC,gBAAgB,CA6clB"}
1
+ {"version":3,"file":"session-tools.d.ts","sourceRoot":"","sources":["../../src/tools/session-tools.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAMhE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAE7E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAQhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AAC1F,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AACvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAElF,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAG7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKnD,sGAAsG;AACtG,eAAO,MAAM,sBAAsB,QAAoD,CAAC;AAExF,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,6BAA6B,CAAC,CAAC;IAC7F,YAAY,EAAE;QACZ,QAAQ,EAAE,sBAAsB,CAAC;QACjC,aAAa,EAAE,oBAAoB,CAAC;QACpC;;;;WAIG;QACH,aAAa,CAAC,EAAE;YACd,WAAW,EAAE,IAAI,CAAC,uBAAuB,EAAE,iBAAiB,CAAC,CAAC;YAC9D,eAAe,CAAC,EAAE,IAAI,CAAC,sBAAsB,EAAE,aAAa,GAAG,MAAM,CAAC,CAAC;YACvE,mBAAmB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;SAC3C,CAAC;KACH,CAAC;IACF,wEAAwE;IACxE,SAAS,EAAE,MAAM,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAClD,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,MAAM,GACN,eAAe,GACf,iBAAiB,GACjB,gCAAgC,GAChC,wBAAwB,GACxB,yBAAyB,GACzB,qBAAqB,GACrB,yBAAyB,GACzB,0BAA0B,GAC1B,yBAAyB,CAC5B,CAAC;IACF,mDAAmD;IACnD,MAAM,EAAE,aAAa,GAAG,kBAAkB,CAAC;IAC3C,aAAa,EAAE,aAAa,CAAC;IAC7B,MAAM,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE,WAAW,CAAC,CAAC;IAC1C,kFAAkF;IAClF,gBAAgB,EAAE,MAAM,IAAI,CAAC,uBAAuB,EAAE,SAAS,CAAC,GAAG,SAAS,CAAC;CAC9E;AA0BD,oGAAoG;AACpG,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,8BAA8B,GACnC,gBAAgB,CA6clB"}
@@ -1,5 +1,6 @@
1
1
  import { boardForWorkItem } from "../boards/semantics.js";
2
2
  import "../boards/index.js";
3
+ import { toBoardSource } from "../boards/project-boards.js";
3
4
  import { workLaneForRole } from "../boards/legacy-plan-role.js";
4
5
  import { primeTenantCredentialsForTenant } from "../routes/tenant-credentials.js";
5
6
  import { effectiveModelScope } from "../storage/domains/projects/base.js";
@@ -273,7 +274,7 @@ function createFactorySessionTools(scope, deps) {
273
274
  const item = await findWorkItem(deps.workItems, scope, workItem);
274
275
  if (!item) throw workItemNotFound(workItem);
275
276
  const label = workItemLabel(item);
276
- const board = deps.boards.get(boardForWorkItem(item));
277
+ const board = (await toBoardSource(deps.boards).load(scope)).get(boardForWorkItem(item));
277
278
  if (!workLaneForRole(board, role) && !board?.phaseForRole(role)) throw new Error(`${label}'s board has no ${role} role.`);
278
279
  const seated = item.sessions[role];
279
280
  if (seated) throw new Error(`${label} already has a ${role} session ${seated.sessionId}; message it with factory_signal_session.`);
@@ -1 +1 @@
1
- {"version":3,"file":"session-tools.js","names":[],"sources":["../../src/tools/session-tools.ts"],"sourcesContent":["import { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport { workLaneForRole } from '../boards/legacy-plan-role.js';\nimport type { BoardRegistry } from '../boards/registry.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { factorySessionUrl, factoryWorkItemUrl } from '../links.js';\nimport { buildModelCatalog, listSessionScopedCredentials } from '../routes/config.js';\nimport { USER_SESSION_BRANCH_PREFIX } from '../routes/source-control-sessions.js';\nimport { primeTenantCredentialsForTenant } from '../routes/tenant-credentials.js';\nimport { FactoryStartTransitionError, WorkItemRepositoryError } from '../rules/start-coordinator.js';\nimport type { FactoryStartCoordinator } from '../rules/start-coordinator.js';\nimport { resolveFactoryModelPolicy } from '../session/credential-policy.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport {\n ensureFactorySourceSession,\n hydrateFactorySession,\n resolveFactorySessionModel,\n} from '../session/factory-session.js';\nimport { recordSessionRunStart } from '../session/run-audit.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { ModelCredentialsStorage } from '../storage/domains/credentials/base.js';\nimport type { CustomProvidersStorage } from '../storage/domains/custom-providers/base.js';\nimport type { IntakeStorage } from '../storage/domains/intake/base.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { deliverFactorySessionNotification, resolveSessionThread } from './deliver.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport { findWorkItem, workItemNotFound, workItemNumber, workItemRefSchema } from './read-tools.js';\nimport { heartbeatOpenRunUntilEnd } from './run-watch.js';\nimport type { FactoryToolScope } from './scope.js';\nimport { sessionModel } from './side-sessions.js';\n\nconst TITLE_LENGTH = 120;\n\n/** Who renews the open-run entries this process writes; one id per process, like the dispatcher's. */\nexport const FACTORY_TOOLS_OWNER_ID = `factory-tools:${globalThis.crypto.randomUUID()}`;\n\nexport interface FactorySessionToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'getSourceControlBySessionId'>;\n sessionModel: {\n projects: FactoryProjectsStorage;\n modelDefaults: ModelDefaultsStorage;\n /**\n * Tenant credential stores, present when auth is on. Model switches then\n * check the target session's credential scope, like the Settings picker;\n * without them the server's own keys decide.\n */\n tenantCatalog?: {\n credentials: Pick<ModelCredentialsStorage, 'listCredentials'>;\n customProviders?: Pick<CustomProvidersStorage, 'ensureReady' | 'list'>;\n deploymentProviders?: ReadonlySet<string>;\n };\n };\n /** Resolved lazily: the controller only exists once the mount is up. */\n messaging: () => Promise<FactoryMessaging | null>;\n workItems: Pick<\n WorkItemsStorage,\n | 'list'\n | 'getForProject'\n | 'listRunBindings'\n | 'findActiveRunBindingForSession'\n | 'findRunBindingByThread'\n | 'hasRunBindingForSession'\n | 'subscribeToWorkItem'\n | 'unsubscribeFromWorkItem'\n | 'listSessionSubscriptions'\n | 'listWorkItemSubscribers'\n >;\n boards: BoardRegistry;\n auditRecorder: AuditRecorder;\n intake?: Pick<IntakeStorage, 'getConfig'>;\n /** The browser Start's coordinator; absent until the Factory runtime is ready. */\n startCoordinator: () => Pick<FactoryStartCoordinator, 'prepare'> | undefined;\n}\n\nconst LISTED_MODELS = 8;\n\nfunction ambiguousRepository(candidates: readonly string[]): string {\n return `This item has several repositories (${candidates.join(', ')}); v1 can't choose one.`;\n}\n\nfunction startErrorMessage(error: unknown): string | null {\n if (error instanceof WorkItemRepositoryError) {\n return error.code === 'repository_ambiguous' ? ambiguousRepository(error.candidates) : error.message;\n }\n if (error instanceof FactoryStartTransitionError) return `The start was refused: ${error.result.reason}`;\n return null;\n}\n\nfunction workItemLabel(item: WorkItemRow): string {\n const number = workItemNumber(item);\n return number === null ? `\"${item.title}\"` : `#${number}`;\n}\n\nfunction titleFrom(prompt: string): string {\n const firstLine = prompt.split('\\n').find(line => line.trim()) ?? prompt;\n return firstLine.trim().slice(0, TITLE_LENGTH);\n}\n\n/** Tools that start sessions on the caller's behalf, or switch their models. User sessions only. */\nexport function createFactorySessionTools(\n scope: FactoryToolScope,\n deps: FactorySessionToolDependencies,\n): IntegrationTools {\n /** The models a session can switch to, resolved from the target session's owner and project policy. */\n async function sessionModelCatalog(\n controller: FactoryMessaging['controller'],\n sessionId: string,\n threadId: string,\n resourceId: string,\n ): Promise<{ usable: string[]; known: Set<string>; scopeLabel: string }> {\n const models = await controller.listAvailableModels();\n const known = new Set(models.flatMap(model => (typeof model.id === 'string' ? [model.id] : [])));\n const tenantCatalog = deps.sessionModel.tenantCatalog;\n if (!tenantCatalog) {\n const usable = (await buildModelCatalog({ controller })).map(model => model.id);\n return { usable, known, scopeLabel: 'server' };\n }\n const requestContext = new RequestContext();\n requestContext.set('user', { id: scope.userId, organizationId: scope.orgId });\n requestContext.set('controller', { sessionId, resourceId, threadId });\n const policy = await resolveFactoryModelPolicy(requestContext, {\n projects: deps.sessionModel.projects,\n sourceControl: deps.sessions,\n workItems: deps.workItems,\n });\n if (!policy) throw new Error('Session model policy is unavailable.');\n const modelScope = policy.tenant.credentialScope ?? 'user';\n const orgId = policy.tenant.orgId ?? scope.orgId;\n const customProviders = tenantCatalog.customProviders;\n const usable = (\n await buildModelCatalog({\n controller,\n tenantCredentials: await listSessionScopedCredentials({\n credentials: tenantCatalog.credentials,\n orgId,\n userId: policy.tenant.userId,\n modelScope,\n }),\n deploymentProviders: tenantCatalog.deploymentProviders,\n listCustomProviders: customProviders\n ? async () => {\n await customProviders.ensureReady();\n return customProviders.list({ orgId });\n }\n : undefined,\n })\n ).map(model => model.id);\n for (const id of usable) known.add(id);\n return { usable, known, scopeLabel: modelScope === 'factory' ? 'Factory' : 'session owner' };\n }\n\n function unrunnableModelError(\n modelId: string,\n catalog: { usable: string[]; known: Set<string>; scopeLabel: string },\n ) {\n const options = catalog.usable.length\n ? ` Models available to it: ${catalog.usable.slice(0, LISTED_MODELS).join(', ')}${catalog.usable.length > LISTED_MODELS ? ', …' : ''}.`\n : ' No models are available to it; connect a provider credential first.';\n return new Error(\n catalog.known.has(modelId)\n ? `Model ${modelId} exists, but this session's credential scope (${catalog.scopeLabel}) has no credential for its provider.${options}`\n : `Unknown model ${modelId}.${options}`,\n );\n }\n\n const switchModel = createTool({\n id: 'factory_switch_session_model',\n description:\n 'Look up or switch the model of a session you own in this Factory (side, work item, or review session). When someone describes a model by purpose, family, or preference rather than an exact ID, call with sessionId alone first to list models the target session can actually use. Interpret their intent against that list, then call with the chosen exact modelId to switch; do not use their words as a guessed model ID. A running session switches immediately; a stopped one uses the model on its next run. User-scoped sessions use their owner credentials; Factory-scoped sessions use Factory credentials. Use a session ID from the Factory session or work item tools.',\n inputSchema: z\n .object({\n sessionId: z.string().trim().min(1).max(200),\n modelId: z.string().trim().min(1).max(200).optional().describe('Omit to list usable models without switching.'),\n })\n .strict(),\n execute: async ({ sessionId, modelId }) => {\n const row = await deps.sessions.getBySessionId(sessionId);\n if (\n !row ||\n row.orgId !== scope.orgId ||\n row.factoryProjectId !== scope.factoryProjectId ||\n row.userId !== scope.userId\n ) {\n throw new Error('Session not found or not owned by you in this Factory.');\n }\n // A turn nobody signed in started reaches only this session and the side sessions it started.\n if (!scope.interactive && sessionId !== scope.sessionId && row.originSessionId !== scope.sessionId) {\n throw new Error(\n 'Without a person in this turn, only this session and the side sessions it started can switch models.',\n );\n }\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Session model switching is not available on this server.');\n const threadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId,\n });\n if (!threadId) throw new Error('Session has no conversation thread yet.');\n // The thread's resource, not the session id: sessions share their Factory's or owner's resource.\n const resourceId = (await messaging.controller.queryThreadById({ threadId }))?.resourceId ?? sessionId;\n const catalog = await sessionModelCatalog(messaging.controller, sessionId, threadId, resourceId);\n if (modelId === undefined) return { sessionId, models: catalog.usable };\n if (!catalog.usable.includes(modelId)) throw unrunnableModelError(modelId, catalog);\n const { live } = await messaging.controller.setThreadModel({ resourceId, threadId, modelId });\n return { sessionId, threadId, modelId, live };\n },\n });\n\n const start = createTool({\n id: 'factory_start_session',\n description: [\n 'Start a side session in this Factory with a prompt. It runs on its own sandbox and branch as you, and reports back to this session with `factory-session` notifications.',\n 'A side session uses this session’s model. Pass `model` to start it on another model the session credentials can run.',\n 'Use it for self-contained investigations; when the result should land as a pull request, file a work item instead.',\n 'With `workItem` and `role` it starts that role on the work item instead, without moving the card, and this session follows the item.',\n \"A role that already has a session on the item (current or past) can't be started again from here: message that session with factory_signal_session, or ask the person to start it from a signed-in chat.\",\n ].join(' '),\n inputSchema: z\n .object({\n prompt: z.string().trim().min(1).max(8000).describe('The first message the new session receives.'),\n title: z.string().trim().min(1).max(TITLE_LENGTH).optional(),\n model: z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe(\n 'Model id for a side session. Omit it to inherit this session’s model. The session credentials must be able to run it.',\n ),\n workItem: workItemRefSchema.optional(),\n role: z.string().trim().min(1).max(64).optional(),\n })\n .strict()\n .refine(input => input.role === undefined || input.workItem !== undefined, {\n message: 'Pass workItem together with role.',\n path: ['role'],\n }),\n execute: async ({ prompt, title, model, workItem, role }) => {\n if (workItem !== undefined) {\n // A role start records a run as the person, so it waits for a turn the person started.\n if (!scope.interactive) {\n throw new Error(\n 'Starting a role on a work item needs a person in this turn; start a side session, or ask the person to ask for it in a signed-in chat.',\n );\n }\n return startWorkItemRole({ prompt, title, workItem, role });\n }\n\n const parent = await deps.sessions.getBySessionId(scope.sessionId);\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!parent || !sourceControl) throw new Error('This session has no Factory session row to start from.');\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Starting sessions is not available on this server.');\n\n // The child runs with the same credentials as this session, so validate against those.\n const parentThreadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId: scope.sessionId,\n });\n const parentResourceId = parentThreadId\n ? ((await messaging.controller.queryThreadById({ threadId: parentThreadId }))?.resourceId ?? scope.sessionId)\n : scope.sessionId;\n const catalog = await sessionModelCatalog(\n messaging.controller,\n scope.sessionId,\n parentThreadId ?? scope.threadId,\n parentResourceId,\n );\n const inherited = model\n ? null\n : await sessionModel(messaging.controller, deps.sessions, deps.workItems, scope.sessionId);\n const requestedModel = model ?? (inherited && catalog.usable.includes(inherited) ? inherited : undefined);\n if (requestedModel && !catalog.usable.includes(requestedModel))\n throw unrunnableModelError(requestedModel, catalog);\n\n const sessionId = globalThis.crypto.randomUUID();\n const ensured = await ensureFactorySourceSession({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,\n // Bills the caller: the credential tenant follows the row's user.\n attributeToUserId: scope.userId,\n // Same repository as the session that started it.\n ...(parent.projectRepositoryId ? { projectRepositoryId: parent.projectRepositoryId } : {}),\n visibility: parent.visibility,\n origin: { sessionId: scope.sessionId, threadId: scope.threadId },\n });\n const resolvedModel = await resolveFactorySessionModel({\n ...deps.sessionModel,\n factoryProjectId: scope.factoryProjectId,\n orgId: scope.orgId,\n ownerUserId: scope.userId,\n // No work item, so an interactive session: a hybrid Factory bills it as one.\n context: 'session',\n });\n\n // Only the new row's owner as `user`. The tool call's context carries the\n // interactive marker and the parent's controller entry; neither may reach the child.\n const kickoffContext = new RequestContext();\n kickoffContext.set('user', { workosId: scope.userId, organizationId: scope.orgId });\n\n const tags = { factoryProjectId: scope.factoryProjectId, projectRepositoryId: ensured.projectRepositoryId };\n const session = await messaging.controller.createSession({\n id: ensured.sessionId,\n ownerId: scope.userId,\n // A side session works for the Factory, so it shares the Factory's memory\n // resource; a private one stays with its owner, like a Slack DM.\n resourceId: parent.visibility === 'private' ? scope.userId : scope.factoryProjectId,\n threadId: ensured.sessionId,\n requestContext: kickoffContext,\n tags,\n });\n await session.state.set(tags);\n await hydrateFactorySession(session, {\n orgId: scope.orgId,\n modelScope: resolvedModel.modelScope,\n ownerUserId: scope.userId,\n defaultModelId: requestedModel ?? resolvedModel.modelId,\n });\n\n const threadId = session.thread.requireId();\n const threadTitle = title ?? titleFrom(prompt);\n await session.thread.rename({ title: threadTitle, pin: false });\n // On the row too, so its run-end reports name it without waiting for the title mirror.\n await sourceControl.sessions.rename({ sessionId: ensured.sessionId, title: threadTitle });\n await session.thread.setSetting({ key: 'factorySessionId', value: ensured.sessionId });\n\n await primeTenantCredentialsForTenant({\n orgId: scope.orgId,\n userId: scope.userId,\n credentialScope: resolvedModel.modelScope,\n });\n // How its runs end reaches this session through the listener every Factory\n // session gets (`observeSessionRunReports`), which follows the row's origin.\n // `requireDelivery` makes `accepted` reject when the wake fails; the run itself is not awaited.\n await session.sendSignal(\n { id: globalThis.crypto.randomUUID(), type: 'user', tagName: 'user', contents: prompt },\n { requestContext: kickoffContext, requireDelivery: true },\n ).accepted;\n\n return {\n mode: 'side' as const,\n sessionId: ensured.sessionId,\n threadId,\n title: threadTitle,\n url:\n factorySessionUrl({ factoryProjectId: scope.factoryProjectId, sessionId: ensured.sessionId, threadId }) ??\n null,\n };\n },\n });\n\n async function startWorkItemRole({\n prompt,\n title,\n workItem,\n role,\n }: {\n prompt: string;\n title?: string;\n workItem: string;\n role?: string;\n }) {\n if (!role) throw new Error('Pass the role to start on the work item.');\n const coordinator = deps.startCoordinator();\n if (!coordinator) throw new Error(\"Factory sessions can't be started yet; try again shortly.\");\n const item = await findWorkItem(deps.workItems, scope, workItem);\n if (!item) throw workItemNotFound(workItem);\n const label = workItemLabel(item);\n const board = deps.boards.get(boardForWorkItem(item));\n // Work's old `plan` role still names Planning, though no phase runs in a `plan` context any more.\n if (!workLaneForRole(board, role) && !board?.phaseForRole(role)) {\n throw new Error(`${label}'s board has no ${role} role.`);\n }\n const seated = item.sessions[role];\n if (seated) {\n throw new Error(\n `${label} already has a ${role} session ${seated.sessionId}; message it with factory_signal_session.`,\n );\n }\n\n const parent = await deps.sessions.getBySessionId(scope.sessionId);\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!parent || !sourceControl) throw new Error('This session has no Factory session row to start from.');\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Starting sessions is not available on this server.');\n const project = await deps.sessionModel.projects.get({ orgId: scope.orgId, id: scope.factoryProjectId });\n if (!project) throw new Error('This Factory no longer exists.');\n // A work-item start is a run, so a hybrid Factory uses the Factory's scope (as the Start route does).\n const modelScope = effectiveModelScope(project.modelScope, 'run');\n const projectRepositoryId = await resolveRepository(sourceControl, item);\n\n const sessionId = globalThis.crypto.randomUUID();\n const ensured = await ensureFactorySourceSession({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,\n projectRepositoryId,\n attributeToUserId: scope.userId,\n // A run works a shared card, so the org can open it and answer its approvals even when the session that\n // started it is private.\n visibility: 'org',\n });\n\n // Follow first, so a fast run's first report already has somewhere to go.\n const followScope = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n const alreadyFollowing = (\n await deps.workItems.listSessionSubscriptions({ ...followScope, sessionId: scope.sessionId })\n ).some(subscription => subscription.workItemId === item.id);\n await deps.workItems.subscribeToWorkItem({\n ...followScope,\n workItemId: item.id,\n sessionId: scope.sessionId,\n threadId: scope.threadId,\n resourceId: scope.resourceId,\n userId: scope.userId,\n reason: 'started',\n });\n\n const startedAt = new Date();\n let prepared;\n try {\n prepared = await coordinator.prepare({\n orgId: scope.orgId,\n userId: scope.userId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: ensured.sessionId,\n threadTitle: title ?? `${label} ${role}`,\n kickoffKey: globalThis.crypto.randomUUID(),\n modelScope,\n defaultModelId: modelScope === 'factory' ? (project.defaultModelId ?? undefined) : undefined,\n workItem: {\n id: item.id,\n role,\n input: {\n externalSource: item.externalSource,\n parentWorkItemId: item.parentWorkItemId,\n title: item.title,\n stages: item.stages,\n sessions: item.sessions,\n metadata: item.metadata,\n },\n },\n });\n } catch (error) {\n if (!alreadyFollowing) {\n await deps.workItems.unsubscribeFromWorkItem({\n orgId: scope.orgId,\n workItemId: item.id,\n sessionId: scope.sessionId,\n });\n }\n const message = startErrorMessage(error);\n if (message) throw new Error(message);\n throw error;\n }\n\n const bindings = await deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id);\n const replaced = bindings.find(\n binding =>\n binding.role === role &&\n binding.id !== prepared.bindingId &&\n binding.revokedAt !== null &&\n binding.revokedAt >= startedAt,\n );\n\n const session = await messaging.controller.getSession(prepared.sessionId);\n if (!session) {\n throw new Error(\n `The ${role} session ${prepared.sessionId} was bound but is not running; message it with factory_signal_session to wake it.`,\n );\n }\n const kickoffId = globalThis.crypto.randomUUID();\n // Heartbeat from before the kickoff so a fast run's end is seen; the open-run\n // entry and audit follow the accepted kickoff, as the dispatcher records them.\n // The run's end reaches this item's followers through the session's listener.\n const heartbeat = heartbeatOpenRunUntilEnd(session, kickoffId);\n const kickoffContext = new RequestContext();\n kickoffContext.set('user', { workosId: scope.userId, organizationId: scope.orgId });\n try {\n await session.sendSignal(\n { id: kickoffId, type: 'user', tagName: 'user', contents: prompt },\n { requestContext: kickoffContext, requireDelivery: true },\n ).accepted;\n } catch (error) {\n heartbeat.stop();\n throw error;\n }\n\n await recordSessionRunStart(session, {\n audit: deps.auditRecorder,\n actorType: 'human',\n observedEnd: heartbeat.endReason,\n run: {\n kickoffId,\n ownerId: FACTORY_TOOLS_OWNER_ID,\n heartbeatAt: Date.now(),\n bindingId: prepared.bindingId,\n role,\n startedBy: scope.userId,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n workItemId: item.id,\n workItemName: item.title,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n branch: prepared.branch,\n },\n });\n\n const number = workItemNumber(item);\n return {\n mode: 'work-item' as const,\n workItem: {\n id: item.id,\n ...(number === null ? {} : { number }),\n url:\n factoryWorkItemUrl({\n factoryProjectId: scope.factoryProjectId,\n board: boardForWorkItem(item),\n workItemId: item.id,\n }) ?? null,\n },\n role,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n url:\n factorySessionUrl({\n factoryProjectId: scope.factoryProjectId,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n }) ?? null,\n bindingId: prepared.bindingId,\n ...(replaced ? { replacedSessionId: replaced.sessionId } : {}),\n };\n }\n\n async function resolveRepository(sourceControl: SourceControlStorageHandle, item: WorkItemRow): Promise<string> {\n const intakeConfig = await deps.intake?.getConfig({ orgId: scope.orgId, integrationIds: ['linear'] });\n const repository = await resolveWorkItemRepository({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n item,\n linearRepositoryMap: intakeConfig?.linear?.repositoryByLinearProject,\n });\n if (repository.status === 'ambiguous') {\n throw new Error(ambiguousRepository(repository.candidates));\n }\n if (repository.status === 'unlinked') throw new Error(repository.hint);\n return repository.projectRepositoryId;\n }\n\n return { factory_start_session: start, factory_switch_session_model: switchModel };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"session-tools.js","names":[],"sources":["../../src/tools/session-tools.ts"],"sourcesContent":["import { RequestContext } from '@mastra/core/request-context';\nimport { createTool } from '@mastra/core/tools';\nimport { z } from 'zod';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport { workLaneForRole } from '../boards/legacy-plan-role.js';\nimport { toBoardSource } from '../boards/project-boards.js';\nimport type { ProjectBoardSource } from '../boards/project-boards.js';\nimport type { BoardRegistry } from '../boards/registry.js';\nimport type { IntegrationTools } from '../integrations/base.js';\nimport { factorySessionUrl, factoryWorkItemUrl } from '../links.js';\nimport { buildModelCatalog, listSessionScopedCredentials } from '../routes/config.js';\nimport { USER_SESSION_BRANCH_PREFIX } from '../routes/source-control-sessions.js';\nimport { primeTenantCredentialsForTenant } from '../routes/tenant-credentials.js';\nimport { FactoryStartTransitionError, WorkItemRepositoryError } from '../rules/start-coordinator.js';\nimport type { FactoryStartCoordinator } from '../rules/start-coordinator.js';\nimport { resolveFactoryModelPolicy } from '../session/credential-policy.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport {\n ensureFactorySourceSession,\n hydrateFactorySession,\n resolveFactorySessionModel,\n} from '../session/factory-session.js';\nimport { recordSessionRunStart } from '../session/run-audit.js';\nimport { resolveWorkItemRepository } from '../session/work-item-repository.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport type { ModelCredentialsStorage } from '../storage/domains/credentials/base.js';\nimport type { CustomProvidersStorage } from '../storage/domains/custom-providers/base.js';\nimport type { IntakeStorage } from '../storage/domains/intake/base.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { deliverFactorySessionNotification, resolveSessionThread } from './deliver.js';\nimport type { FactoryMessaging } from './messaging-tools.js';\nimport { findWorkItem, workItemNotFound, workItemNumber, workItemRefSchema } from './read-tools.js';\nimport { heartbeatOpenRunUntilEnd } from './run-watch.js';\nimport type { FactoryToolScope } from './scope.js';\nimport { sessionModel } from './side-sessions.js';\n\nconst TITLE_LENGTH = 120;\n\n/** Who renews the open-run entries this process writes; one id per process, like the dispatcher's. */\nexport const FACTORY_TOOLS_OWNER_ID = `factory-tools:${globalThis.crypto.randomUUID()}`;\n\nexport interface FactorySessionToolDependencies {\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'getSourceControlBySessionId'>;\n sessionModel: {\n projects: FactoryProjectsStorage;\n modelDefaults: ModelDefaultsStorage;\n /**\n * Tenant credential stores, present when auth is on. Model switches then\n * check the target session's credential scope, like the Settings picker;\n * without them the server's own keys decide.\n */\n tenantCatalog?: {\n credentials: Pick<ModelCredentialsStorage, 'listCredentials'>;\n customProviders?: Pick<CustomProvidersStorage, 'ensureReady' | 'list'>;\n deploymentProviders?: ReadonlySet<string>;\n };\n };\n /** Resolved lazily: the controller only exists once the mount is up. */\n messaging: () => Promise<FactoryMessaging | null>;\n workItems: Pick<\n WorkItemsStorage,\n | 'list'\n | 'getForProject'\n | 'listRunBindings'\n | 'findActiveRunBindingForSession'\n | 'findRunBindingByThread'\n | 'hasRunBindingForSession'\n | 'subscribeToWorkItem'\n | 'unsubscribeFromWorkItem'\n | 'listSessionSubscriptions'\n | 'listWorkItemSubscribers'\n >;\n /** A fixed registry, or the per-project boards. */\n boards: BoardRegistry | ProjectBoardSource;\n auditRecorder: AuditRecorder;\n intake?: Pick<IntakeStorage, 'getConfig'>;\n /** The browser Start's coordinator; absent until the Factory runtime is ready. */\n startCoordinator: () => Pick<FactoryStartCoordinator, 'prepare'> | undefined;\n}\n\nconst LISTED_MODELS = 8;\n\nfunction ambiguousRepository(candidates: readonly string[]): string {\n return `This item has several repositories (${candidates.join(', ')}); v1 can't choose one.`;\n}\n\nfunction startErrorMessage(error: unknown): string | null {\n if (error instanceof WorkItemRepositoryError) {\n return error.code === 'repository_ambiguous' ? ambiguousRepository(error.candidates) : error.message;\n }\n if (error instanceof FactoryStartTransitionError) return `The start was refused: ${error.result.reason}`;\n return null;\n}\n\nfunction workItemLabel(item: WorkItemRow): string {\n const number = workItemNumber(item);\n return number === null ? `\"${item.title}\"` : `#${number}`;\n}\n\nfunction titleFrom(prompt: string): string {\n const firstLine = prompt.split('\\n').find(line => line.trim()) ?? prompt;\n return firstLine.trim().slice(0, TITLE_LENGTH);\n}\n\n/** Tools that start sessions on the caller's behalf, or switch their models. User sessions only. */\nexport function createFactorySessionTools(\n scope: FactoryToolScope,\n deps: FactorySessionToolDependencies,\n): IntegrationTools {\n /** The models a session can switch to, resolved from the target session's owner and project policy. */\n async function sessionModelCatalog(\n controller: FactoryMessaging['controller'],\n sessionId: string,\n threadId: string,\n resourceId: string,\n ): Promise<{ usable: string[]; known: Set<string>; scopeLabel: string }> {\n const models = await controller.listAvailableModels();\n const known = new Set(models.flatMap(model => (typeof model.id === 'string' ? [model.id] : [])));\n const tenantCatalog = deps.sessionModel.tenantCatalog;\n if (!tenantCatalog) {\n const usable = (await buildModelCatalog({ controller })).map(model => model.id);\n return { usable, known, scopeLabel: 'server' };\n }\n const requestContext = new RequestContext();\n requestContext.set('user', { id: scope.userId, organizationId: scope.orgId });\n requestContext.set('controller', { sessionId, resourceId, threadId });\n const policy = await resolveFactoryModelPolicy(requestContext, {\n projects: deps.sessionModel.projects,\n sourceControl: deps.sessions,\n workItems: deps.workItems,\n });\n if (!policy) throw new Error('Session model policy is unavailable.');\n const modelScope = policy.tenant.credentialScope ?? 'user';\n const orgId = policy.tenant.orgId ?? scope.orgId;\n const customProviders = tenantCatalog.customProviders;\n const usable = (\n await buildModelCatalog({\n controller,\n tenantCredentials: await listSessionScopedCredentials({\n credentials: tenantCatalog.credentials,\n orgId,\n userId: policy.tenant.userId,\n modelScope,\n }),\n deploymentProviders: tenantCatalog.deploymentProviders,\n listCustomProviders: customProviders\n ? async () => {\n await customProviders.ensureReady();\n return customProviders.list({ orgId });\n }\n : undefined,\n })\n ).map(model => model.id);\n for (const id of usable) known.add(id);\n return { usable, known, scopeLabel: modelScope === 'factory' ? 'Factory' : 'session owner' };\n }\n\n function unrunnableModelError(\n modelId: string,\n catalog: { usable: string[]; known: Set<string>; scopeLabel: string },\n ) {\n const options = catalog.usable.length\n ? ` Models available to it: ${catalog.usable.slice(0, LISTED_MODELS).join(', ')}${catalog.usable.length > LISTED_MODELS ? ', …' : ''}.`\n : ' No models are available to it; connect a provider credential first.';\n return new Error(\n catalog.known.has(modelId)\n ? `Model ${modelId} exists, but this session's credential scope (${catalog.scopeLabel}) has no credential for its provider.${options}`\n : `Unknown model ${modelId}.${options}`,\n );\n }\n\n const switchModel = createTool({\n id: 'factory_switch_session_model',\n description:\n 'Look up or switch the model of a session you own in this Factory (side, work item, or review session). When someone describes a model by purpose, family, or preference rather than an exact ID, call with sessionId alone first to list models the target session can actually use. Interpret their intent against that list, then call with the chosen exact modelId to switch; do not use their words as a guessed model ID. A running session switches immediately; a stopped one uses the model on its next run. User-scoped sessions use their owner credentials; Factory-scoped sessions use Factory credentials. Use a session ID from the Factory session or work item tools.',\n inputSchema: z\n .object({\n sessionId: z.string().trim().min(1).max(200),\n modelId: z.string().trim().min(1).max(200).optional().describe('Omit to list usable models without switching.'),\n })\n .strict(),\n execute: async ({ sessionId, modelId }) => {\n const row = await deps.sessions.getBySessionId(sessionId);\n if (\n !row ||\n row.orgId !== scope.orgId ||\n row.factoryProjectId !== scope.factoryProjectId ||\n row.userId !== scope.userId\n ) {\n throw new Error('Session not found or not owned by you in this Factory.');\n }\n // A turn nobody signed in started reaches only this session and the side sessions it started.\n if (!scope.interactive && sessionId !== scope.sessionId && row.originSessionId !== scope.sessionId) {\n throw new Error(\n 'Without a person in this turn, only this session and the side sessions it started can switch models.',\n );\n }\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Session model switching is not available on this server.');\n const threadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId,\n });\n if (!threadId) throw new Error('Session has no conversation thread yet.');\n // The thread's resource, not the session id: sessions share their Factory's or owner's resource.\n const resourceId = (await messaging.controller.queryThreadById({ threadId }))?.resourceId ?? sessionId;\n const catalog = await sessionModelCatalog(messaging.controller, sessionId, threadId, resourceId);\n if (modelId === undefined) return { sessionId, models: catalog.usable };\n if (!catalog.usable.includes(modelId)) throw unrunnableModelError(modelId, catalog);\n const { live } = await messaging.controller.setThreadModel({ resourceId, threadId, modelId });\n return { sessionId, threadId, modelId, live };\n },\n });\n\n const start = createTool({\n id: 'factory_start_session',\n description: [\n 'Start a side session in this Factory with a prompt. It runs on its own sandbox and branch as you, and reports back to this session with `factory-session` notifications.',\n 'A side session uses this session’s model. Pass `model` to start it on another model the session credentials can run.',\n 'Use it for self-contained investigations; when the result should land as a pull request, file a work item instead.',\n 'With `workItem` and `role` it starts that role on the work item instead, without moving the card, and this session follows the item.',\n \"A role that already has a session on the item (current or past) can't be started again from here: message that session with factory_signal_session, or ask the person to start it from a signed-in chat.\",\n ].join(' '),\n inputSchema: z\n .object({\n prompt: z.string().trim().min(1).max(8000).describe('The first message the new session receives.'),\n title: z.string().trim().min(1).max(TITLE_LENGTH).optional(),\n model: z\n .string()\n .trim()\n .min(1)\n .max(200)\n .optional()\n .describe(\n 'Model id for a side session. Omit it to inherit this session’s model. The session credentials must be able to run it.',\n ),\n workItem: workItemRefSchema.optional(),\n role: z.string().trim().min(1).max(64).optional(),\n })\n .strict()\n .refine(input => input.role === undefined || input.workItem !== undefined, {\n message: 'Pass workItem together with role.',\n path: ['role'],\n }),\n execute: async ({ prompt, title, model, workItem, role }) => {\n if (workItem !== undefined) {\n // A role start records a run as the person, so it waits for a turn the person started.\n if (!scope.interactive) {\n throw new Error(\n 'Starting a role on a work item needs a person in this turn; start a side session, or ask the person to ask for it in a signed-in chat.',\n );\n }\n return startWorkItemRole({ prompt, title, workItem, role });\n }\n\n const parent = await deps.sessions.getBySessionId(scope.sessionId);\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!parent || !sourceControl) throw new Error('This session has no Factory session row to start from.');\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Starting sessions is not available on this server.');\n\n // The child runs with the same credentials as this session, so validate against those.\n const parentThreadId = await resolveSessionThread({\n controller: messaging.controller,\n sessions: deps.sessions,\n workItems: deps.workItems,\n sessionId: scope.sessionId,\n });\n const parentResourceId = parentThreadId\n ? ((await messaging.controller.queryThreadById({ threadId: parentThreadId }))?.resourceId ?? scope.sessionId)\n : scope.sessionId;\n const catalog = await sessionModelCatalog(\n messaging.controller,\n scope.sessionId,\n parentThreadId ?? scope.threadId,\n parentResourceId,\n );\n const inherited = model\n ? null\n : await sessionModel(messaging.controller, deps.sessions, deps.workItems, scope.sessionId);\n const requestedModel = model ?? (inherited && catalog.usable.includes(inherited) ? inherited : undefined);\n if (requestedModel && !catalog.usable.includes(requestedModel))\n throw unrunnableModelError(requestedModel, catalog);\n\n const sessionId = globalThis.crypto.randomUUID();\n const ensured = await ensureFactorySourceSession({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,\n // Bills the caller: the credential tenant follows the row's user.\n attributeToUserId: scope.userId,\n // Same repository as the session that started it.\n ...(parent.projectRepositoryId ? { projectRepositoryId: parent.projectRepositoryId } : {}),\n visibility: parent.visibility,\n origin: { sessionId: scope.sessionId, threadId: scope.threadId },\n });\n const resolvedModel = await resolveFactorySessionModel({\n ...deps.sessionModel,\n factoryProjectId: scope.factoryProjectId,\n orgId: scope.orgId,\n ownerUserId: scope.userId,\n // No work item, so an interactive session: a hybrid Factory bills it as one.\n context: 'session',\n });\n\n // Only the new row's owner as `user`. The tool call's context carries the\n // interactive marker and the parent's controller entry; neither may reach the child.\n const kickoffContext = new RequestContext();\n kickoffContext.set('user', { workosId: scope.userId, organizationId: scope.orgId });\n\n const tags = { factoryProjectId: scope.factoryProjectId, projectRepositoryId: ensured.projectRepositoryId };\n const session = await messaging.controller.createSession({\n id: ensured.sessionId,\n ownerId: scope.userId,\n // A side session works for the Factory, so it shares the Factory's memory\n // resource; a private one stays with its owner, like a Slack DM.\n resourceId: parent.visibility === 'private' ? scope.userId : scope.factoryProjectId,\n threadId: ensured.sessionId,\n requestContext: kickoffContext,\n tags,\n });\n await session.state.set(tags);\n await hydrateFactorySession(session, {\n orgId: scope.orgId,\n modelScope: resolvedModel.modelScope,\n ownerUserId: scope.userId,\n defaultModelId: requestedModel ?? resolvedModel.modelId,\n });\n\n const threadId = session.thread.requireId();\n const threadTitle = title ?? titleFrom(prompt);\n await session.thread.rename({ title: threadTitle, pin: false });\n // On the row too, so its run-end reports name it without waiting for the title mirror.\n await sourceControl.sessions.rename({ sessionId: ensured.sessionId, title: threadTitle });\n await session.thread.setSetting({ key: 'factorySessionId', value: ensured.sessionId });\n\n await primeTenantCredentialsForTenant({\n orgId: scope.orgId,\n userId: scope.userId,\n credentialScope: resolvedModel.modelScope,\n });\n // How its runs end reaches this session through the listener every Factory\n // session gets (`observeSessionRunReports`), which follows the row's origin.\n // `requireDelivery` makes `accepted` reject when the wake fails; the run itself is not awaited.\n await session.sendSignal(\n { id: globalThis.crypto.randomUUID(), type: 'user', tagName: 'user', contents: prompt },\n { requestContext: kickoffContext, requireDelivery: true },\n ).accepted;\n\n return {\n mode: 'side' as const,\n sessionId: ensured.sessionId,\n threadId,\n title: threadTitle,\n url:\n factorySessionUrl({ factoryProjectId: scope.factoryProjectId, sessionId: ensured.sessionId, threadId }) ??\n null,\n };\n },\n });\n\n async function startWorkItemRole({\n prompt,\n title,\n workItem,\n role,\n }: {\n prompt: string;\n title?: string;\n workItem: string;\n role?: string;\n }) {\n if (!role) throw new Error('Pass the role to start on the work item.');\n const coordinator = deps.startCoordinator();\n if (!coordinator) throw new Error(\"Factory sessions can't be started yet; try again shortly.\");\n const item = await findWorkItem(deps.workItems, scope, workItem);\n if (!item) throw workItemNotFound(workItem);\n const label = workItemLabel(item);\n const board = (await toBoardSource(deps.boards).load(scope)).get(boardForWorkItem(item));\n // Work's old `plan` role still names Planning, though no phase runs in a `plan` context any more.\n if (!workLaneForRole(board, role) && !board?.phaseForRole(role)) {\n throw new Error(`${label}'s board has no ${role} role.`);\n }\n const seated = item.sessions[role];\n if (seated) {\n throw new Error(\n `${label} already has a ${role} session ${seated.sessionId}; message it with factory_signal_session.`,\n );\n }\n\n const parent = await deps.sessions.getBySessionId(scope.sessionId);\n const sourceControl = await deps.sessions.getSourceControlBySessionId(scope.sessionId);\n if (!parent || !sourceControl) throw new Error('This session has no Factory session row to start from.');\n const messaging = await deps.messaging();\n if (!messaging) throw new Error('Starting sessions is not available on this server.');\n const project = await deps.sessionModel.projects.get({ orgId: scope.orgId, id: scope.factoryProjectId });\n if (!project) throw new Error('This Factory no longer exists.');\n // A work-item start is a run, so a hybrid Factory uses the Factory's scope (as the Start route does).\n const modelScope = effectiveModelScope(project.modelScope, 'run');\n const projectRepositoryId = await resolveRepository(sourceControl, item);\n\n const sessionId = globalThis.crypto.randomUUID();\n const ensured = await ensureFactorySourceSession({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId,\n branch: `${USER_SESSION_BRANCH_PREFIX}${sessionId}`,\n projectRepositoryId,\n attributeToUserId: scope.userId,\n // A run works a shared card, so the org can open it and answer its approvals even when the session that\n // started it is private.\n visibility: 'org',\n });\n\n // Follow first, so a fast run's first report already has somewhere to go.\n const followScope = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n const alreadyFollowing = (\n await deps.workItems.listSessionSubscriptions({ ...followScope, sessionId: scope.sessionId })\n ).some(subscription => subscription.workItemId === item.id);\n await deps.workItems.subscribeToWorkItem({\n ...followScope,\n workItemId: item.id,\n sessionId: scope.sessionId,\n threadId: scope.threadId,\n resourceId: scope.resourceId,\n userId: scope.userId,\n reason: 'started',\n });\n\n const startedAt = new Date();\n let prepared;\n try {\n prepared = await coordinator.prepare({\n orgId: scope.orgId,\n userId: scope.userId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: ensured.sessionId,\n threadTitle: title ?? `${label} ${role}`,\n kickoffKey: globalThis.crypto.randomUUID(),\n modelScope,\n defaultModelId: modelScope === 'factory' ? (project.defaultModelId ?? undefined) : undefined,\n workItem: {\n id: item.id,\n role,\n input: {\n externalSource: item.externalSource,\n parentWorkItemId: item.parentWorkItemId,\n title: item.title,\n stages: item.stages,\n sessions: item.sessions,\n metadata: item.metadata,\n },\n },\n });\n } catch (error) {\n if (!alreadyFollowing) {\n await deps.workItems.unsubscribeFromWorkItem({\n orgId: scope.orgId,\n workItemId: item.id,\n sessionId: scope.sessionId,\n });\n }\n const message = startErrorMessage(error);\n if (message) throw new Error(message);\n throw error;\n }\n\n const bindings = await deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id);\n const replaced = bindings.find(\n binding =>\n binding.role === role &&\n binding.id !== prepared.bindingId &&\n binding.revokedAt !== null &&\n binding.revokedAt >= startedAt,\n );\n\n const session = await messaging.controller.getSession(prepared.sessionId);\n if (!session) {\n throw new Error(\n `The ${role} session ${prepared.sessionId} was bound but is not running; message it with factory_signal_session to wake it.`,\n );\n }\n const kickoffId = globalThis.crypto.randomUUID();\n // Heartbeat from before the kickoff so a fast run's end is seen; the open-run\n // entry and audit follow the accepted kickoff, as the dispatcher records them.\n // The run's end reaches this item's followers through the session's listener.\n const heartbeat = heartbeatOpenRunUntilEnd(session, kickoffId);\n const kickoffContext = new RequestContext();\n kickoffContext.set('user', { workosId: scope.userId, organizationId: scope.orgId });\n try {\n await session.sendSignal(\n { id: kickoffId, type: 'user', tagName: 'user', contents: prompt },\n { requestContext: kickoffContext, requireDelivery: true },\n ).accepted;\n } catch (error) {\n heartbeat.stop();\n throw error;\n }\n\n await recordSessionRunStart(session, {\n audit: deps.auditRecorder,\n actorType: 'human',\n observedEnd: heartbeat.endReason,\n run: {\n kickoffId,\n ownerId: FACTORY_TOOLS_OWNER_ID,\n heartbeatAt: Date.now(),\n bindingId: prepared.bindingId,\n role,\n startedBy: scope.userId,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n workItemId: item.id,\n workItemName: item.title,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n branch: prepared.branch,\n },\n });\n\n const number = workItemNumber(item);\n return {\n mode: 'work-item' as const,\n workItem: {\n id: item.id,\n ...(number === null ? {} : { number }),\n url:\n factoryWorkItemUrl({\n factoryProjectId: scope.factoryProjectId,\n board: boardForWorkItem(item),\n workItemId: item.id,\n }) ?? null,\n },\n role,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n url:\n factorySessionUrl({\n factoryProjectId: scope.factoryProjectId,\n sessionId: prepared.sessionId,\n threadId: prepared.threadId,\n }) ?? null,\n bindingId: prepared.bindingId,\n ...(replaced ? { replacedSessionId: replaced.sessionId } : {}),\n };\n }\n\n async function resolveRepository(sourceControl: SourceControlStorageHandle, item: WorkItemRow): Promise<string> {\n const intakeConfig = await deps.intake?.getConfig({ orgId: scope.orgId, integrationIds: ['linear'] });\n const repository = await resolveWorkItemRepository({\n sourceControl,\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n item,\n linearRepositoryMap: intakeConfig?.linear?.repositoryByLinearProject,\n });\n if (repository.status === 'ambiguous') {\n throw new Error(ambiguousRepository(repository.candidates));\n }\n if (repository.status === 'unlinked') throw new Error(repository.hint);\n return repository.projectRepositoryId;\n }\n\n return { factory_start_session: start, factory_switch_session_model: switchModel };\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import type { NotificationsStorage } from '@mastra/core/notifications';
2
2
  import type { ComputeStateSignalArgs, ComputeStateSignalResult, Processor } from '@mastra/core/processors';
3
+ import type { ProjectBoardSource } from '../boards/project-boards.js';
3
4
  import type { BoardRegistry } from '../boards/registry.js';
4
5
  import type { SourceControlSessionLookup } from '../session/factory-session.js';
5
6
  import type { WorkItemsStorage } from '../storage/domains/work-items/base.js';
@@ -16,7 +17,8 @@ export interface FactorySessionsStateDependencies {
16
17
  sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'listByOrigin'>;
17
18
  projects: ResolveFactoryToolScopeOptions['projects'];
18
19
  workItems: Pick<WorkItemsStorage, 'hasRunBindingForSession' | 'listSessionSubscriptions' | 'getForProject' | 'listRunBindings' | 'findActiveRunBindingForSession' | 'listWorkItemSubscribers'>;
19
- boards: BoardRegistry;
20
+ /** A fixed registry, or the per-project boards. */
21
+ boards: BoardRegistry | ProjectBoardSource;
20
22
  /** Whether the session's agent is mid-run in this process; `idle` when it isn't live here. */
21
23
  isRunning: (sessionId: string) => Promise<boolean>;
22
24
  /** When a thread last changed; tells a long run that is still moving from one that stopped. */
@@ -1 +1 @@
1
- {"version":3,"file":"sessions-state-processor.d.ts","sourceRoot":"","sources":["../../src/tools/sessions-state-processor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAG3G,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,OAAO,KAAK,EAAoB,8BAA8B,EAAE,MAAM,YAAY,CAAC;AAWnF,+FAA+F;AAC/F,eAAO,MAAM,2BAA2B,QAAsB,CAAC;AAI/D,8GAA8G;AAC9G,eAAO,MAAM,yBAAyB,QAAiB,CAAC;AACxD,0DAA0D;AAC1D,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAG5C,YAAY,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAEjE,MAAM,WAAW,gCAAgC;IAC/C,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,cAAc,CAAC,CAAC;IAC9E,QAAQ,EAAE,8BAA8B,CAAC,UAAU,CAAC,CAAC;IACrD,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,yBAAyB,GACzB,0BAA0B,GAC1B,eAAe,GACf,iBAAiB,GACjB,gCAAgC,GAChC,yBAAyB,CAC5B,CAAC;IACF,MAAM,EAAE,aAAa,CAAC;IACtB,8FAA8F;IAC9F,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,+FAA+F;IAC/F,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC1D,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,GAAG,IAAI,CAAC,CAAC;IACrF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAiCD;;;;;;GAMG;AACH,qBAAa,6BAA8B,YAAW,SAAS,CAAC,kBAAkB,CAAC;;IACjF,QAAQ,CAAC,EAAE,sBAAY;IACvB,QAAQ,CAAC,OAAO,sBAAY;IAG5B,YAAY,IAAI,EAAE,gCAAgC,EAEjD;IAEK,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAyCxF;CAgKF"}
1
+ {"version":3,"file":"sessions-state-processor.d.ts","sourceRoot":"","sources":["../../src/tools/sessions-state-processor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AACvE,OAAO,KAAK,EAAE,sBAAsB,EAAE,wBAAwB,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAI3G,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAChF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE9E,OAAO,KAAK,EAAoB,8BAA8B,EAAE,MAAM,YAAY,CAAC;AAWnF,+FAA+F;AAC/F,eAAO,MAAM,2BAA2B,QAAsB,CAAC;AAI/D,8GAA8G;AAC9G,eAAO,MAAM,yBAAyB,QAAiB,CAAC;AACxD,0DAA0D;AAC1D,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAG5C,YAAY,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAEjE,MAAM,WAAW,gCAAgC;IAC/C,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,gBAAgB,GAAG,cAAc,CAAC,CAAC;IAC9E,QAAQ,EAAE,8BAA8B,CAAC,UAAU,CAAC,CAAC;IACrD,SAAS,EAAE,IAAI,CACb,gBAAgB,EACd,yBAAyB,GACzB,0BAA0B,GAC1B,eAAe,GACf,iBAAiB,GACjB,gCAAgC,GAChC,yBAAyB,CAC5B,CAAC;IACF,mDAAmD;IACnD,MAAM,EAAE,aAAa,GAAG,kBAAkB,CAAC;IAC3C,8FAA8F;IAC9F,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,+FAA+F;IAC/F,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IAC1D,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,mBAAmB,CAAC,GAAG,IAAI,CAAC,CAAC;IACrF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAiCD;;;;;;GAMG;AACH,qBAAa,6BAA8B,YAAW,SAAS,CAAC,kBAAkB,CAAC;;IACjF,QAAQ,CAAC,EAAE,sBAAY;IACvB,QAAQ,CAAC,OAAO,sBAAY;IAG5B,YAAY,IAAI,EAAE,gCAAgC,EAEjD;IAEK,kBAAkB,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAyCxF;CAiKF"}
@@ -1,5 +1,6 @@
1
1
  import { boardForWorkItem } from "../boards/semantics.js";
2
2
  import "../boards/index.js";
3
+ import { toBoardSource } from "../boards/project-boards.js";
3
4
  import { resolveFactoryToolScope } from "./scope.js";
4
5
  import { itemLabel, lastReport, listSessionNotifications, listSideSessions, newestNotification } from "./side-sessions.js";
5
6
  import { createHash } from "crypto";
@@ -126,7 +127,7 @@ var FactorySessionsStateProcessor = class {
126
127
  const item = await this.#deps.workItems.getForProject(scope.orgId, scope.factoryProjectId, subscription.workItemId);
127
128
  if (!item) continue;
128
129
  const stage = item.stages[0] ?? "unknown";
129
- const terminal = this.#deps.boards.get(boardForWorkItem(item))?.isTerminal(stage) ?? false;
130
+ const terminal = (await toBoardSource(this.#deps.boards).load(scope)).get(boardForWorkItem(item))?.isTerminal(stage) ?? false;
130
131
  if (terminal && now - item.updatedAt.getTime() > 864e5) continue;
131
132
  const active = (await this.#deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id)).find((binding) => binding.status === "active");
132
133
  const label = itemLabel(item);
@@ -1 +1 @@
1
- {"version":3,"file":"sessions-state-processor.js","names":["newest","report"],"sources":["../../src/tools/sessions-state-processor.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { NotificationsStorage } from '@mastra/core/notifications';\nimport type { ComputeStateSignalArgs, ComputeStateSignalResult, Processor } from '@mastra/core/processors';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/registry.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { resolveFactoryToolScope } from './scope.js';\nimport type { FactoryToolScope, ResolveFactoryToolScopeOptions } from './scope.js';\nimport {\n itemLabel,\n lastReport as report,\n listSessionNotifications,\n listSideSessions,\n newestNotification as newest,\n} from './side-sessions.js';\nimport type { FactorySessionRunState, LastReport } from './side-sessions.js';\n\nconst STATE_ID = 'factory-sessions';\n/** Finished side sessions and items that reached a terminal stage drop out after this long. */\nexport const FACTORY_SESSIONS_MAX_AGE_MS = 24 * 60 * 60 * 1000;\n/** Asks a long run to show it is still moving; a silent half hour reads the same as a stuck one. */\nconst PROGRESS_ASK = 'On long work, send a progress report after each plan step.';\n\n/** A running worker whose thread has not moved for this long is shown as quiet, so a stuck run stands out. */\nexport const FACTORY_SESSIONS_QUIET_MS = 10 * 60 * 1000;\n/** Rows shown before the rest collapse into \"+N more\". */\nexport const FACTORY_SESSIONS_MAX_ROWS = 20;\nconst EMPTY_CACHE_KEY = 'empty';\n\nexport type { FactorySessionRunState } from './side-sessions.js';\n\nexport interface FactorySessionsStateDependencies {\n authEnabled: boolean;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'listByOrigin'>;\n projects: ResolveFactoryToolScopeOptions['projects'];\n workItems: Pick<\n WorkItemsStorage,\n | 'hasRunBindingForSession'\n | 'listSessionSubscriptions'\n | 'getForProject'\n | 'listRunBindings'\n | 'findActiveRunBindingForSession'\n | 'listWorkItemSubscribers'\n >;\n boards: BoardRegistry;\n /** Whether the session's agent is mid-run in this process; `idle` when it isn't live here. */\n isRunning: (sessionId: string) => Promise<boolean>;\n /** When a thread last changed; tells a long run that is still moving from one that stopped. */\n lastActivity?: (threadId: string) => Promise<Date | null>;\n notifications: () => Promise<Pick<NotificationsStorage, 'listNotifications'> | null>;\n now?: () => number;\n}\n\ninterface SideSessionRow {\n sessionId: string;\n title: string;\n state: FactorySessionRunState;\n lastReport: LastReport | null;\n muted: boolean;\n}\n\ninterface FollowedRow {\n workItemId: string;\n label: string;\n title: string;\n stage: string;\n worker: { role: string; sessionId: string; state: FactorySessionRunState; quietSince: string | null } | null;\n lastReport: LastReport | null;\n}\n\ninterface SessionsState {\n sideSessions: SideSessionRow[];\n following: FollowedRow[];\n more: number;\n}\n\nfunction escapeText(value: string): string {\n return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');\n}\n\nfunction cacheKeyFor(value: unknown): string {\n return `sessions:${createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 16)}`;\n}\n\n/**\n * The `<factory-sessions>` state signal. A user session sees the side sessions\n * it started and the work items it follows; a side session or item worker sees\n * who it reports to. Sessions it only messaged never appear. Silent until there\n * is something to show; when the last entry ages out it sends one final empty\n * snapshot, then stays silent.\n */\nexport class FactorySessionsStateProcessor implements Processor<'factory-sessions'> {\n readonly id = STATE_ID;\n readonly stateId = STATE_ID;\n readonly #deps: FactorySessionsStateDependencies;\n\n constructor(deps: FactorySessionsStateDependencies) {\n this.#deps = deps;\n }\n\n async computeStateSignal(args: ComputeStateSignalArgs): Promise<ComputeStateSignalResult> {\n const scope = await resolveFactoryToolScope({\n requestContext: args.requestContext!,\n authEnabled: this.#deps.authEnabled,\n sessions: this.#deps.sessions,\n projects: this.#deps.projects,\n workItems: this.#deps.workItems,\n });\n if (!scope) return;\n\n const hasBase = Boolean(args.lastSnapshot) && args.contextWindow.hasSnapshot;\n const previousKey = hasBase\n ? (args.tracking?.currentCacheKey ?? (args.lastSnapshot?.metadata?.state as { cacheKey?: string })?.cacheKey)\n : undefined;\n const unchanged = (cacheKey: string) => previousKey === cacheKey;\n // Silence rule: one final empty snapshot replaces a non-empty one, then nothing.\n const cleared = () => hasBase && previousKey !== undefined && previousKey !== EMPTY_CACHE_KEY;\n\n if (scope.kind === 'worker') {\n const contents = await this.#workerLine(scope);\n if (!contents) {\n return cleared()\n ? this.#signal(EMPTY_CACHE_KEY, 'This session is not reporting on any work now.', {}, { entries: 0 })\n : undefined;\n }\n const cacheKey = cacheKeyFor(contents);\n if (unchanged(cacheKey)) return;\n return this.#signal(cacheKey, escapeText(contents), { worker: contents }, { entries: 1 });\n }\n\n const state = await this.#userState(scope);\n if (state.sideSessions.length === 0 && state.following.length === 0) {\n if (!cleared()) return;\n return this.#signal(EMPTY_CACHE_KEY, 'No sub-sessions or followed work items.', state, { entries: 0 });\n }\n const cacheKey = cacheKeyFor(state);\n if (unchanged(cacheKey)) return;\n return this.#signal(cacheKey, this.#render(state), state, {\n sideSessions: state.sideSessions.length,\n following: state.following.length,\n });\n }\n\n #signal(cacheKey: string, contents: string, value: unknown, attributes: Record<string, number>) {\n return {\n id: STATE_ID,\n cacheKey,\n mode: 'snapshot' as const,\n tagName: 'factory-sessions',\n contents,\n value: { sessions: value },\n attributes,\n metadata: { value: { sessions: value } },\n };\n }\n\n async #workerLine(scope: FactoryToolScope): Promise<string | null> {\n if (scope.origin) {\n return `Started by session ${scope.origin.sessionId} (thread ${scope.origin.threadId}). Report back with factory_message_origin. ${PROGRESS_ASK}`;\n }\n const binding = await this.#deps.workItems.findActiveRunBindingForSession({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n });\n if (!binding) return null;\n const item = await this.#deps.workItems.getForProject(scope.orgId, scope.factoryProjectId, binding.workItemId);\n const subscribers = await this.#deps.workItems.listWorkItemSubscribers({\n orgId: scope.orgId,\n workItemId: binding.workItemId,\n });\n const following = subscribers.filter(subscriber => subscriber.sessionId !== scope.sessionId).length;\n // Nobody to report to: nothing worth showing (most board-started workers).\n if (following === 0) return null;\n const label = item ? itemLabel(item) : binding.workItemId;\n return `Working on ${label} as ${binding.role}. Reports go to ${following} following session${following === 1 ? '' : 's'}. ${PROGRESS_ASK}`;\n }\n\n async #userState(scope: FactoryToolScope): Promise<SessionsState> {\n const now = this.#deps.now?.() ?? Date.now();\n const notifications = await this.#deps.notifications();\n const records = await listSessionNotifications(notifications, scope.threadId);\n\n const sideSessions: Array<SideSessionRow & { active: boolean }> = [];\n const listed = await listSideSessions(\n {\n sessions: this.#deps.sessions,\n workItems: this.#deps.workItems,\n isRunning: this.#deps.isRunning,\n notifications,\n },\n scope,\n { records },\n );\n for (const row of listed) {\n if (row.state !== 'running' && now - Date.parse(row.lastActivityAt) > FACTORY_SESSIONS_MAX_AGE_MS) continue;\n sideSessions.push({\n sessionId: row.sessionId,\n title: row.title,\n state: row.state,\n lastReport: row.lastReport,\n muted: row.mutedAt !== null,\n active: row.state !== 'idle',\n });\n }\n\n const subscriptions = await this.#deps.workItems.listSessionSubscriptions({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n });\n const following: Array<FollowedRow & { active: boolean }> = [];\n for (const subscription of subscriptions) {\n const item = await this.#deps.workItems.getForProject(\n scope.orgId,\n scope.factoryProjectId,\n subscription.workItemId,\n );\n if (!item) continue;\n const stage = item.stages[0] ?? 'unknown';\n const terminal = this.#deps.boards.get(boardForWorkItem(item))?.isTerminal(stage) ?? false;\n if (terminal && now - item.updatedAt.getTime() > FACTORY_SESSIONS_MAX_AGE_MS) continue;\n const bindings = await this.#deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id);\n const active = bindings.find(binding => binding.status === 'active');\n const label = itemLabel(item);\n const last = newest(\n records,\n record =>\n record.attributes?.['work-item'] === label || (record.kind === 'stage' && record.sourceId === item.id),\n );\n const running = active ? await this.#deps.isRunning(active.sessionId) : false;\n const lastActivity = active && running ? await this.#deps.lastActivity?.(active.threadId) : null;\n // Only a quiet run is shown with its time; a moving one would change the snapshot on every step.\n const quiet = lastActivity && now - lastActivity.getTime() >= FACTORY_SESSIONS_QUIET_MS ? lastActivity : null;\n const lastFromWorker = active\n ? newest(records, record => record.attributes?.['from-session'] === active.sessionId)\n : undefined;\n following.push({\n workItemId: item.id,\n label,\n title: item.title,\n stage,\n worker: active\n ? {\n role: active.role,\n sessionId: active.sessionId,\n state: running ? 'running' : lastFromWorker?.kind === 'blocked' ? 'parked' : 'idle',\n quietSince: quiet ? quiet.toISOString() : null,\n }\n : null,\n lastReport: report(last),\n active: running && !terminal,\n });\n }\n\n // Active entries first, then within the cap; the rest collapse into \"+N more\".\n const ordered = [\n ...sideSessions.map(row => ({ section: 'side' as const, row })),\n ...following.map(row => ({ section: 'following' as const, row })),\n ].sort((a, b) => Number(b.row.active) - Number(a.row.active));\n const shown = ordered.slice(0, FACTORY_SESSIONS_MAX_ROWS);\n const strip = <T extends { active: boolean }>({ active: _active, ...row }: T) => row;\n return {\n sideSessions: shown.flatMap(entry =>\n entry.section === 'side' ? [strip(entry.row as SideSessionRow & { active: boolean })] : [],\n ),\n following: shown.flatMap(entry =>\n entry.section === 'following' ? [strip(entry.row as FollowedRow & { active: boolean })] : [],\n ),\n more: ordered.length - shown.length,\n };\n }\n\n #render(state: SessionsState): string {\n const lastReport = (last: LastReport | null) => (last ? `; last report ${last.kind} at ${last.at}` : '');\n const lines: string[] = [];\n if (state.sideSessions.length > 0) {\n lines.push('Side sessions you started:');\n for (const row of state.sideSessions) {\n lines.push(\n `- \"${escapeText(row.title)}\" (session ${row.sessionId}): ${row.state}${row.muted ? ' (muted)' : ''}${lastReport(row.lastReport)}`,\n );\n }\n }\n if (state.following.length > 0) {\n lines.push('Work items you follow:');\n for (const row of state.following) {\n const worker = row.worker\n ? `; ${escapeText(row.worker.role)} session ${row.worker.sessionId} ${row.worker.state}${row.worker.quietSince ? ` but quiet since ${row.worker.quietSince}` : ''}`\n : '';\n lines.push(\n `- ${escapeText(row.label)} \"${escapeText(row.title)}\": ${escapeText(row.stage)}${worker}${lastReport(row.lastReport)}`,\n );\n }\n }\n if (state.more > 0) lines.push(`+${state.more} more`);\n lines.push(\n 'Message any of these sessions with factory_signal_session; read full reports with notification_inbox; list side sessions with factory_list_side_sessions; on a turn a person started, mute one with factory_unsubscribe_session.',\n );\n return lines.join('\\n');\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sessions-state-processor.js","names":["newest","report"],"sources":["../../src/tools/sessions-state-processor.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { NotificationsStorage } from '@mastra/core/notifications';\nimport type { ComputeStateSignalArgs, ComputeStateSignalResult, Processor } from '@mastra/core/processors';\n\nimport { boardForWorkItem } from '../boards/index.js';\nimport { toBoardSource } from '../boards/project-boards.js';\nimport type { ProjectBoardSource } from '../boards/project-boards.js';\nimport type { BoardRegistry } from '../boards/registry.js';\nimport type { SourceControlSessionLookup } from '../session/factory-session.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { resolveFactoryToolScope } from './scope.js';\nimport type { FactoryToolScope, ResolveFactoryToolScopeOptions } from './scope.js';\nimport {\n itemLabel,\n lastReport as report,\n listSessionNotifications,\n listSideSessions,\n newestNotification as newest,\n} from './side-sessions.js';\nimport type { FactorySessionRunState, LastReport } from './side-sessions.js';\n\nconst STATE_ID = 'factory-sessions';\n/** Finished side sessions and items that reached a terminal stage drop out after this long. */\nexport const FACTORY_SESSIONS_MAX_AGE_MS = 24 * 60 * 60 * 1000;\n/** Asks a long run to show it is still moving; a silent half hour reads the same as a stuck one. */\nconst PROGRESS_ASK = 'On long work, send a progress report after each plan step.';\n\n/** A running worker whose thread has not moved for this long is shown as quiet, so a stuck run stands out. */\nexport const FACTORY_SESSIONS_QUIET_MS = 10 * 60 * 1000;\n/** Rows shown before the rest collapse into \"+N more\". */\nexport const FACTORY_SESSIONS_MAX_ROWS = 20;\nconst EMPTY_CACHE_KEY = 'empty';\n\nexport type { FactorySessionRunState } from './side-sessions.js';\n\nexport interface FactorySessionsStateDependencies {\n authEnabled: boolean;\n sessions: Pick<SourceControlSessionLookup, 'getBySessionId' | 'listByOrigin'>;\n projects: ResolveFactoryToolScopeOptions['projects'];\n workItems: Pick<\n WorkItemsStorage,\n | 'hasRunBindingForSession'\n | 'listSessionSubscriptions'\n | 'getForProject'\n | 'listRunBindings'\n | 'findActiveRunBindingForSession'\n | 'listWorkItemSubscribers'\n >;\n /** A fixed registry, or the per-project boards. */\n boards: BoardRegistry | ProjectBoardSource;\n /** Whether the session's agent is mid-run in this process; `idle` when it isn't live here. */\n isRunning: (sessionId: string) => Promise<boolean>;\n /** When a thread last changed; tells a long run that is still moving from one that stopped. */\n lastActivity?: (threadId: string) => Promise<Date | null>;\n notifications: () => Promise<Pick<NotificationsStorage, 'listNotifications'> | null>;\n now?: () => number;\n}\n\ninterface SideSessionRow {\n sessionId: string;\n title: string;\n state: FactorySessionRunState;\n lastReport: LastReport | null;\n muted: boolean;\n}\n\ninterface FollowedRow {\n workItemId: string;\n label: string;\n title: string;\n stage: string;\n worker: { role: string; sessionId: string; state: FactorySessionRunState; quietSince: string | null } | null;\n lastReport: LastReport | null;\n}\n\ninterface SessionsState {\n sideSessions: SideSessionRow[];\n following: FollowedRow[];\n more: number;\n}\n\nfunction escapeText(value: string): string {\n return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');\n}\n\nfunction cacheKeyFor(value: unknown): string {\n return `sessions:${createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 16)}`;\n}\n\n/**\n * The `<factory-sessions>` state signal. A user session sees the side sessions\n * it started and the work items it follows; a side session or item worker sees\n * who it reports to. Sessions it only messaged never appear. Silent until there\n * is something to show; when the last entry ages out it sends one final empty\n * snapshot, then stays silent.\n */\nexport class FactorySessionsStateProcessor implements Processor<'factory-sessions'> {\n readonly id = STATE_ID;\n readonly stateId = STATE_ID;\n readonly #deps: FactorySessionsStateDependencies;\n\n constructor(deps: FactorySessionsStateDependencies) {\n this.#deps = deps;\n }\n\n async computeStateSignal(args: ComputeStateSignalArgs): Promise<ComputeStateSignalResult> {\n const scope = await resolveFactoryToolScope({\n requestContext: args.requestContext!,\n authEnabled: this.#deps.authEnabled,\n sessions: this.#deps.sessions,\n projects: this.#deps.projects,\n workItems: this.#deps.workItems,\n });\n if (!scope) return;\n\n const hasBase = Boolean(args.lastSnapshot) && args.contextWindow.hasSnapshot;\n const previousKey = hasBase\n ? (args.tracking?.currentCacheKey ?? (args.lastSnapshot?.metadata?.state as { cacheKey?: string })?.cacheKey)\n : undefined;\n const unchanged = (cacheKey: string) => previousKey === cacheKey;\n // Silence rule: one final empty snapshot replaces a non-empty one, then nothing.\n const cleared = () => hasBase && previousKey !== undefined && previousKey !== EMPTY_CACHE_KEY;\n\n if (scope.kind === 'worker') {\n const contents = await this.#workerLine(scope);\n if (!contents) {\n return cleared()\n ? this.#signal(EMPTY_CACHE_KEY, 'This session is not reporting on any work now.', {}, { entries: 0 })\n : undefined;\n }\n const cacheKey = cacheKeyFor(contents);\n if (unchanged(cacheKey)) return;\n return this.#signal(cacheKey, escapeText(contents), { worker: contents }, { entries: 1 });\n }\n\n const state = await this.#userState(scope);\n if (state.sideSessions.length === 0 && state.following.length === 0) {\n if (!cleared()) return;\n return this.#signal(EMPTY_CACHE_KEY, 'No sub-sessions or followed work items.', state, { entries: 0 });\n }\n const cacheKey = cacheKeyFor(state);\n if (unchanged(cacheKey)) return;\n return this.#signal(cacheKey, this.#render(state), state, {\n sideSessions: state.sideSessions.length,\n following: state.following.length,\n });\n }\n\n #signal(cacheKey: string, contents: string, value: unknown, attributes: Record<string, number>) {\n return {\n id: STATE_ID,\n cacheKey,\n mode: 'snapshot' as const,\n tagName: 'factory-sessions',\n contents,\n value: { sessions: value },\n attributes,\n metadata: { value: { sessions: value } },\n };\n }\n\n async #workerLine(scope: FactoryToolScope): Promise<string | null> {\n if (scope.origin) {\n return `Started by session ${scope.origin.sessionId} (thread ${scope.origin.threadId}). Report back with factory_message_origin. ${PROGRESS_ASK}`;\n }\n const binding = await this.#deps.workItems.findActiveRunBindingForSession({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n });\n if (!binding) return null;\n const item = await this.#deps.workItems.getForProject(scope.orgId, scope.factoryProjectId, binding.workItemId);\n const subscribers = await this.#deps.workItems.listWorkItemSubscribers({\n orgId: scope.orgId,\n workItemId: binding.workItemId,\n });\n const following = subscribers.filter(subscriber => subscriber.sessionId !== scope.sessionId).length;\n // Nobody to report to: nothing worth showing (most board-started workers).\n if (following === 0) return null;\n const label = item ? itemLabel(item) : binding.workItemId;\n return `Working on ${label} as ${binding.role}. Reports go to ${following} following session${following === 1 ? '' : 's'}. ${PROGRESS_ASK}`;\n }\n\n async #userState(scope: FactoryToolScope): Promise<SessionsState> {\n const now = this.#deps.now?.() ?? Date.now();\n const notifications = await this.#deps.notifications();\n const records = await listSessionNotifications(notifications, scope.threadId);\n\n const sideSessions: Array<SideSessionRow & { active: boolean }> = [];\n const listed = await listSideSessions(\n {\n sessions: this.#deps.sessions,\n workItems: this.#deps.workItems,\n isRunning: this.#deps.isRunning,\n notifications,\n },\n scope,\n { records },\n );\n for (const row of listed) {\n if (row.state !== 'running' && now - Date.parse(row.lastActivityAt) > FACTORY_SESSIONS_MAX_AGE_MS) continue;\n sideSessions.push({\n sessionId: row.sessionId,\n title: row.title,\n state: row.state,\n lastReport: row.lastReport,\n muted: row.mutedAt !== null,\n active: row.state !== 'idle',\n });\n }\n\n const subscriptions = await this.#deps.workItems.listSessionSubscriptions({\n orgId: scope.orgId,\n factoryProjectId: scope.factoryProjectId,\n sessionId: scope.sessionId,\n });\n const following: Array<FollowedRow & { active: boolean }> = [];\n for (const subscription of subscriptions) {\n const item = await this.#deps.workItems.getForProject(\n scope.orgId,\n scope.factoryProjectId,\n subscription.workItemId,\n );\n if (!item) continue;\n const stage = item.stages[0] ?? 'unknown';\n const boards = await toBoardSource(this.#deps.boards).load(scope);\n const terminal = boards.get(boardForWorkItem(item))?.isTerminal(stage) ?? false;\n if (terminal && now - item.updatedAt.getTime() > FACTORY_SESSIONS_MAX_AGE_MS) continue;\n const bindings = await this.#deps.workItems.listRunBindings(scope.orgId, scope.factoryProjectId, item.id);\n const active = bindings.find(binding => binding.status === 'active');\n const label = itemLabel(item);\n const last = newest(\n records,\n record =>\n record.attributes?.['work-item'] === label || (record.kind === 'stage' && record.sourceId === item.id),\n );\n const running = active ? await this.#deps.isRunning(active.sessionId) : false;\n const lastActivity = active && running ? await this.#deps.lastActivity?.(active.threadId) : null;\n // Only a quiet run is shown with its time; a moving one would change the snapshot on every step.\n const quiet = lastActivity && now - lastActivity.getTime() >= FACTORY_SESSIONS_QUIET_MS ? lastActivity : null;\n const lastFromWorker = active\n ? newest(records, record => record.attributes?.['from-session'] === active.sessionId)\n : undefined;\n following.push({\n workItemId: item.id,\n label,\n title: item.title,\n stage,\n worker: active\n ? {\n role: active.role,\n sessionId: active.sessionId,\n state: running ? 'running' : lastFromWorker?.kind === 'blocked' ? 'parked' : 'idle',\n quietSince: quiet ? quiet.toISOString() : null,\n }\n : null,\n lastReport: report(last),\n active: running && !terminal,\n });\n }\n\n // Active entries first, then within the cap; the rest collapse into \"+N more\".\n const ordered = [\n ...sideSessions.map(row => ({ section: 'side' as const, row })),\n ...following.map(row => ({ section: 'following' as const, row })),\n ].sort((a, b) => Number(b.row.active) - Number(a.row.active));\n const shown = ordered.slice(0, FACTORY_SESSIONS_MAX_ROWS);\n const strip = <T extends { active: boolean }>({ active: _active, ...row }: T) => row;\n return {\n sideSessions: shown.flatMap(entry =>\n entry.section === 'side' ? [strip(entry.row as SideSessionRow & { active: boolean })] : [],\n ),\n following: shown.flatMap(entry =>\n entry.section === 'following' ? [strip(entry.row as FollowedRow & { active: boolean })] : [],\n ),\n more: ordered.length - shown.length,\n };\n }\n\n #render(state: SessionsState): string {\n const lastReport = (last: LastReport | null) => (last ? `; last report ${last.kind} at ${last.at}` : '');\n const lines: string[] = [];\n if (state.sideSessions.length > 0) {\n lines.push('Side sessions you started:');\n for (const row of state.sideSessions) {\n lines.push(\n `- \"${escapeText(row.title)}\" (session ${row.sessionId}): ${row.state}${row.muted ? ' (muted)' : ''}${lastReport(row.lastReport)}`,\n );\n }\n }\n if (state.following.length > 0) {\n lines.push('Work items you follow:');\n for (const row of state.following) {\n const worker = row.worker\n ? `; ${escapeText(row.worker.role)} session ${row.worker.sessionId} ${row.worker.state}${row.worker.quietSince ? ` but quiet since ${row.worker.quietSince}` : ''}`\n : '';\n lines.push(\n `- ${escapeText(row.label)} \"${escapeText(row.title)}\": ${escapeText(row.stage)}${worker}${lastReport(row.lastReport)}`,\n );\n }\n }\n if (state.more > 0) lines.push(`+${state.more} more`);\n lines.push(\n 'Message any of these sessions with factory_signal_session; read full reports with notification_inbox; list side sessions with factory_list_side_sessions; on a turn a person started, mute one with factory_unsubscribe_session.',\n );\n return lines.join('\\n');\n }\n}\n"],"mappings":"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"}
@@ -4,6 +4,7 @@
4
4
  * through the same code the board's routes use.
5
5
  */
6
6
  import type { BoardRegistry } from '../boards/index.js';
7
+ import type { ProjectBoardSource } from '../boards/project-boards.js';
7
8
  import type { IntegrationTools } from '../integrations/base.js';
8
9
  import type { FactoryTransitionService } from '../rules/transition-service.js';
9
10
  import type { SourceControlSessionLookup } from '../session/factory-session.js';
@@ -13,7 +14,8 @@ import type { WorkItemsStorage } from '../storage/domains/work-items/base.js';
13
14
  import type { FactoryToolScope } from './scope.js';
14
15
  export interface FactoryWorkItemToolDependencies {
15
16
  workItems: WorkItemsStorage;
16
- boards: BoardRegistry;
17
+ /** A fixed registry, or the per-project boards. */
18
+ boards: BoardRegistry | ProjectBoardSource;
17
19
  /** Governed stage moves; filing and moving items refuse while it is unavailable. */
18
20
  transitionService: Pick<FactoryTransitionService, 'transition'> | undefined;
19
21
  /** Writes audit rows with an explicit actor; tool calls have no HTTP request to read one from. */
@@ -1 +1 @@
1
- {"version":3,"file":"work-item-tools.d.ts","sourceRoot":"","sources":["../../src/tools/work-item-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC/E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAE5E,OAAO,KAAK,EAAoC,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAmBhH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,gBAAgB,CAAC;IAC5B,MAAM,EAAE,aAAa,CAAC;IACtB,oFAAoF;IACpF,iBAAiB,EAAE,IAAI,CAAC,wBAAwB,EAAE,YAAY,CAAC,GAAG,SAAS,CAAC;IAC5E,kGAAkG;IAClG,aAAa,EAAE,aAAa,CAAC;IAC7B,4FAA4F;IAC5F,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;IAC5C,qGAAqG;IACrG,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,6BAA6B,CAAC,CAAC;CAC3E;AAiCD,8EAA8E;AAC9E,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,+BAA+B,GACpC,gBAAgB,CAgTlB"}
1
+ {"version":3,"file":"work-item-tools.d.ts","sourceRoot":"","sources":["../../src/tools/work-item-tools.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAC/E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,+BAA+B,CAAC;AAEhF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAC;AACxE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAE5E,OAAO,KAAK,EAAoC,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAmBhH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,MAAM,WAAW,+BAA+B;IAC9C,SAAS,EAAE,gBAAgB,CAAC;IAC5B,mDAAmD;IACnD,MAAM,EAAE,aAAa,GAAG,kBAAkB,CAAC;IAC3C,oFAAoF;IACpF,iBAAiB,EAAE,IAAI,CAAC,wBAAwB,EAAE,YAAY,CAAC,GAAG,SAAS,CAAC;IAC5E,kGAAkG;IAClG,aAAa,EAAE,aAAa,CAAC;IAC7B,4FAA4F;IAC5F,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;IAC5C,qGAAqG;IACrG,QAAQ,EAAE,IAAI,CAAC,0BAA0B,EAAE,6BAA6B,CAAC,CAAC;CAC3E;AAiCD,8EAA8E;AAC9E,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,+BAA+B,GACpC,gBAAgB,CAuTlB"}