@mastra/factory 0.24.0-alpha.1 → 0.24.0-alpha.10

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 (389) hide show
  1. package/README.md +106 -4
  2. package/dist/auth.d.ts +14 -0
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +45 -9
  5. package/dist/auth.js.map +1 -1
  6. package/dist/boards/review.d.ts.map +1 -1
  7. package/dist/boards/review.js +21 -1
  8. package/dist/boards/review.js.map +1 -1
  9. package/dist/boards/work-transition-policy.d.ts.map +1 -1
  10. package/dist/boards/work-transition-policy.js +12 -1
  11. package/dist/boards/work-transition-policy.js.map +1 -1
  12. package/dist/boards/work.d.ts +14 -0
  13. package/dist/boards/work.d.ts.map +1 -1
  14. package/dist/boards/work.js +70 -7
  15. package/dist/boards/work.js.map +1 -1
  16. package/dist/capabilities/version-control.d.ts +26 -0
  17. package/dist/capabilities/version-control.d.ts.map +1 -1
  18. package/dist/factory.d.ts +8 -0
  19. package/dist/factory.d.ts.map +1 -1
  20. package/dist/factory.js +354 -27
  21. package/dist/factory.js.map +1 -1
  22. package/dist/integrations/base.d.ts +1 -1
  23. package/dist/integrations/base.d.ts.map +1 -1
  24. package/dist/integrations/github/default-rules.d.ts.map +1 -1
  25. package/dist/integrations/github/default-rules.js +1 -16
  26. package/dist/integrations/github/default-rules.js.map +1 -1
  27. package/dist/integrations/github/integration.d.ts.map +1 -1
  28. package/dist/integrations/github/integration.js +37 -1
  29. package/dist/integrations/github/integration.js.map +1 -1
  30. package/dist/integrations/github/routes.d.ts +1 -1
  31. package/dist/integrations/github/routes.d.ts.map +1 -1
  32. package/dist/integrations/github/routes.js +19 -11
  33. package/dist/integrations/github/routes.js.map +1 -1
  34. package/dist/integrations/github/rules.d.ts.map +1 -1
  35. package/dist/integrations/github/rules.js +26 -22
  36. package/dist/integrations/github/rules.js.map +1 -1
  37. package/dist/integrations/github/sandbox-release.d.ts +15 -3
  38. package/dist/integrations/github/sandbox-release.d.ts.map +1 -1
  39. package/dist/integrations/github/sandbox-release.js +20 -3
  40. package/dist/integrations/github/sandbox-release.js.map +1 -1
  41. package/dist/integrations/github/sandbox.d.ts +2 -3
  42. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  43. package/dist/integrations/github/sandbox.js +78 -7
  44. package/dist/integrations/github/sandbox.js.map +1 -1
  45. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
  46. package/dist/integrations/github/session-subscriptions.js +1 -2
  47. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  48. package/dist/integrations/github/subscriptions.d.ts +7 -4
  49. package/dist/integrations/github/subscriptions.d.ts.map +1 -1
  50. package/dist/integrations/github/subscriptions.js +5 -5
  51. package/dist/integrations/github/subscriptions.js.map +1 -1
  52. package/dist/integrations/github/webhook.d.ts +4 -2
  53. package/dist/integrations/github/webhook.d.ts.map +1 -1
  54. package/dist/integrations/github/webhook.js +59 -36
  55. package/dist/integrations/github/webhook.js.map +1 -1
  56. package/dist/integrations/gitlab/api.d.ts +16 -1
  57. package/dist/integrations/gitlab/api.d.ts.map +1 -1
  58. package/dist/integrations/gitlab/api.js +9 -0
  59. package/dist/integrations/gitlab/api.js.map +1 -1
  60. package/dist/integrations/gitlab/routes.d.ts.map +1 -1
  61. package/dist/integrations/gitlab/routes.js +10 -6
  62. package/dist/integrations/gitlab/routes.js.map +1 -1
  63. package/dist/integrations/gitlab/rules.d.ts.map +1 -1
  64. package/dist/integrations/gitlab/rules.js +28 -21
  65. package/dist/integrations/gitlab/rules.js.map +1 -1
  66. package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -1
  67. package/dist/integrations/gitlab/session-subscriptions.js +1 -2
  68. package/dist/integrations/gitlab/session-subscriptions.js.map +1 -1
  69. package/dist/integrations/gitlab/subscriptions.d.ts +7 -4
  70. package/dist/integrations/gitlab/subscriptions.d.ts.map +1 -1
  71. package/dist/integrations/gitlab/subscriptions.js +5 -5
  72. package/dist/integrations/gitlab/subscriptions.js.map +1 -1
  73. package/dist/integrations/gitlab/version-control.d.ts.map +1 -1
  74. package/dist/integrations/gitlab/version-control.js +37 -3
  75. package/dist/integrations/gitlab/version-control.js.map +1 -1
  76. package/dist/integrations/incidentio/intake.d.ts +1 -1
  77. package/dist/integrations/incidentio/intake.d.ts.map +1 -1
  78. package/dist/integrations/incidentio/intake.js.map +1 -1
  79. package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
  80. package/dist/integrations/linear/agent-tools.js.map +1 -1
  81. package/dist/integrations/platform/connect/routes.d.ts +1 -1
  82. package/dist/integrations/platform/connect/routes.d.ts.map +1 -1
  83. package/dist/integrations/platform/connect/routes.js.map +1 -1
  84. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  85. package/dist/integrations/platform/github/integration.js +27 -1
  86. package/dist/integrations/platform/github/integration.js.map +1 -1
  87. package/dist/integrations/platform/incidentio/integration.d.ts +1 -1
  88. package/dist/integrations/platform/incidentio/integration.d.ts.map +1 -1
  89. package/dist/integrations/platform/incidentio/integration.js.map +1 -1
  90. package/dist/integrations/platform/jira/integration.d.ts +1 -1
  91. package/dist/integrations/platform/jira/integration.d.ts.map +1 -1
  92. package/dist/integrations/platform/jira/integration.js.map +1 -1
  93. package/dist/integrations/platform/linear/event-worker.d.ts +1 -1
  94. package/dist/integrations/platform/linear/event-worker.d.ts.map +1 -1
  95. package/dist/integrations/platform/linear/event-worker.js.map +1 -1
  96. package/dist/integrations/slack/integration.d.ts.map +1 -1
  97. package/dist/integrations/slack/integration.js +33 -6
  98. package/dist/integrations/slack/integration.js.map +1 -1
  99. package/dist/integrations/slack/slack.d.ts +16 -5
  100. package/dist/integrations/slack/slack.d.ts.map +1 -1
  101. package/dist/integrations/slack/slack.js +137 -64
  102. package/dist/integrations/slack/slack.js.map +1 -1
  103. package/dist/integrations/subscription-session.d.ts +14 -0
  104. package/dist/integrations/subscription-session.d.ts.map +1 -1
  105. package/dist/integrations/subscription-session.js +30 -16
  106. package/dist/integrations/subscription-session.js.map +1 -1
  107. package/dist/mcp/disconnect.d.ts +10 -0
  108. package/dist/mcp/disconnect.d.ts.map +1 -0
  109. package/dist/mcp/disconnect.js +44 -0
  110. package/dist/mcp/disconnect.js.map +1 -0
  111. package/dist/mcp/routes.d.ts +46 -0
  112. package/dist/mcp/routes.d.ts.map +1 -0
  113. package/dist/mcp/routes.js +243 -0
  114. package/dist/mcp/routes.js.map +1 -0
  115. package/dist/provider-display-name.d.ts +2 -0
  116. package/dist/provider-display-name.d.ts.map +1 -0
  117. package/dist/provider-display-name.js +16 -0
  118. package/dist/provider-display-name.js.map +1 -0
  119. package/dist/review-verdict.d.ts +11 -0
  120. package/dist/review-verdict.d.ts.map +1 -1
  121. package/dist/review-verdict.js +16 -1
  122. package/dist/review-verdict.js.map +1 -1
  123. package/dist/routes/attention-activity.d.ts.map +1 -1
  124. package/dist/routes/attention-activity.js +5 -2
  125. package/dist/routes/attention-activity.js.map +1 -1
  126. package/dist/routes/attention-providers.d.ts +0 -1
  127. package/dist/routes/attention-providers.d.ts.map +1 -1
  128. package/dist/routes/attention-providers.js +5 -6
  129. package/dist/routes/attention-providers.js.map +1 -1
  130. package/dist/routes/config.d.ts +44 -5
  131. package/dist/routes/config.d.ts.map +1 -1
  132. package/dist/routes/config.js +181 -58
  133. package/dist/routes/config.js.map +1 -1
  134. package/dist/routes/contracts.d.ts +4 -0
  135. package/dist/routes/contracts.d.ts.map +1 -1
  136. package/dist/routes/contracts.js +3 -1
  137. package/dist/routes/contracts.js.map +1 -1
  138. package/dist/routes/custom-provider-source.d.ts +8 -0
  139. package/dist/routes/custom-provider-source.d.ts.map +1 -1
  140. package/dist/routes/custom-provider-source.js +22 -1
  141. package/dist/routes/custom-provider-source.js.map +1 -1
  142. package/dist/routes/oauth.js +1 -1
  143. package/dist/routes/oauth.js.map +1 -1
  144. package/dist/routes/projects.d.ts +1 -1
  145. package/dist/routes/projects.d.ts.map +1 -1
  146. package/dist/routes/projects.js +1 -1
  147. package/dist/routes/projects.js.map +1 -1
  148. package/dist/routes/provider-credentials.d.ts +7 -2
  149. package/dist/routes/provider-credentials.d.ts.map +1 -1
  150. package/dist/routes/provider-credentials.js +30 -8
  151. package/dist/routes/provider-credentials.js.map +1 -1
  152. package/dist/routes/skills.d.ts +2 -4
  153. package/dist/routes/skills.d.ts.map +1 -1
  154. package/dist/routes/skills.js +19 -65
  155. package/dist/routes/skills.js.map +1 -1
  156. package/dist/routes/source-control-sessions.d.ts +9 -1
  157. package/dist/routes/source-control-sessions.d.ts.map +1 -1
  158. package/dist/routes/source-control-sessions.js +131 -7
  159. package/dist/routes/source-control-sessions.js.map +1 -1
  160. package/dist/routes/surface.d.ts +3 -2
  161. package/dist/routes/surface.d.ts.map +1 -1
  162. package/dist/routes/surface.js +7 -3
  163. package/dist/routes/surface.js.map +1 -1
  164. package/dist/routes/work-items.d.ts.map +1 -1
  165. package/dist/routes/work-items.js +60 -120
  166. package/dist/routes/work-items.js.map +1 -1
  167. package/dist/rules/binding-context.js +7 -7
  168. package/dist/rules/binding-context.js.map +1 -1
  169. package/dist/rules/dispatch-errors.d.ts.map +1 -1
  170. package/dist/rules/dispatch-errors.js +12 -0
  171. package/dist/rules/dispatch-errors.js.map +1 -1
  172. package/dist/rules/dispatcher.d.ts +8 -1
  173. package/dist/rules/dispatcher.d.ts.map +1 -1
  174. package/dist/rules/dispatcher.js +117 -12
  175. package/dist/rules/dispatcher.js.map +1 -1
  176. package/dist/rules/processor.d.ts.map +1 -1
  177. package/dist/rules/processor.js +2 -1
  178. package/dist/rules/processor.js.map +1 -1
  179. package/dist/rules/start-coordinator.d.ts +4 -0
  180. package/dist/rules/start-coordinator.d.ts.map +1 -1
  181. package/dist/rules/start-coordinator.js +36 -4
  182. package/dist/rules/start-coordinator.js.map +1 -1
  183. package/dist/rules/tools.d.ts.map +1 -1
  184. package/dist/rules/tools.js +82 -3
  185. package/dist/rules/tools.js.map +1 -1
  186. package/dist/rules/transition-service.d.ts +17 -0
  187. package/dist/rules/transition-service.d.ts.map +1 -1
  188. package/dist/rules/transition-service.js +25 -0
  189. package/dist/rules/transition-service.js.map +1 -1
  190. package/dist/rules/types.d.ts +2 -4
  191. package/dist/rules/types.d.ts.map +1 -1
  192. package/dist/rules/types.js +1 -4
  193. package/dist/rules/types.js.map +1 -1
  194. package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -1
  195. package/dist/sandbox/local-factory-sandbox.js +1 -0
  196. package/dist/sandbox/local-factory-sandbox.js.map +1 -1
  197. package/dist/sandbox/session-retirement.d.ts +27 -3
  198. package/dist/sandbox/session-retirement.d.ts.map +1 -1
  199. package/dist/sandbox/session-retirement.js +158 -20
  200. package/dist/sandbox/session-retirement.js.map +1 -1
  201. package/dist/secret-encryption.d.ts.map +1 -1
  202. package/dist/secret-encryption.js +65 -25
  203. package/dist/secret-encryption.js.map +1 -1
  204. package/dist/session/channel-reply-processor.d.ts +36 -0
  205. package/dist/session/channel-reply-processor.d.ts.map +1 -0
  206. package/dist/session/channel-reply-processor.js +46 -0
  207. package/dist/session/channel-reply-processor.js.map +1 -0
  208. package/dist/session/channel-session-link-processor.d.ts +39 -0
  209. package/dist/session/channel-session-link-processor.d.ts.map +1 -0
  210. package/dist/session/channel-session-link-processor.js +100 -0
  211. package/dist/session/channel-session-link-processor.js.map +1 -0
  212. package/dist/session/credential-policy.d.ts +1 -1
  213. package/dist/session/credential-policy.d.ts.map +1 -1
  214. package/dist/session/credential-policy.js +7 -6
  215. package/dist/session/credential-policy.js.map +1 -1
  216. package/dist/session/default-model-hydration.d.ts +1 -0
  217. package/dist/session/default-model-hydration.d.ts.map +1 -1
  218. package/dist/session/default-model-hydration.js +3 -2
  219. package/dist/session/default-model-hydration.js.map +1 -1
  220. package/dist/session/environment-state-processor.js +1 -1
  221. package/dist/session/environment-state-processor.js.map +1 -1
  222. package/dist/session/factory-session.d.ts +19 -1
  223. package/dist/session/factory-session.d.ts.map +1 -1
  224. package/dist/session/factory-session.js +5 -2
  225. package/dist/session/factory-session.js.map +1 -1
  226. package/dist/session/filesystem-capture.d.ts +3 -3
  227. package/dist/session/filesystem-capture.d.ts.map +1 -1
  228. package/dist/session/filesystem-capture.js +16 -17
  229. package/dist/session/filesystem-capture.js.map +1 -1
  230. package/dist/session/first-exec-capture.d.ts +1 -1
  231. package/dist/session/first-exec-capture.d.ts.map +1 -1
  232. package/dist/session/first-exec-capture.js +1 -1
  233. package/dist/session/first-exec-capture.js.map +1 -1
  234. package/dist/session/first-message-capture.d.ts +1 -1
  235. package/dist/session/first-message-capture.d.ts.map +1 -1
  236. package/dist/session/first-message-capture.js +1 -1
  237. package/dist/session/first-message-capture.js.map +1 -1
  238. package/dist/session/live-sessions.d.ts +1 -1
  239. package/dist/session/live-sessions.js +1 -1
  240. package/dist/session/live-sessions.js.map +1 -1
  241. package/dist/session/memory-settings-hydration.d.ts +1 -1
  242. package/dist/session/memory-settings-hydration.d.ts.map +1 -1
  243. package/dist/session/memory-settings-hydration.js +1 -1
  244. package/dist/session/memory-settings-hydration.js.map +1 -1
  245. package/dist/session/reply-credential-processor.d.ts +15 -0
  246. package/dist/session/reply-credential-processor.d.ts.map +1 -0
  247. package/dist/session/reply-credential-processor.js +38 -0
  248. package/dist/session/reply-credential-processor.js.map +1 -0
  249. package/dist/session/resource-migration.d.ts +87 -0
  250. package/dist/session/resource-migration.d.ts.map +1 -0
  251. package/dist/session/resource-migration.js +163 -0
  252. package/dist/session/resource-migration.js.map +1 -0
  253. package/dist/session/run-audit.d.ts +24 -0
  254. package/dist/session/run-audit.d.ts.map +1 -1
  255. package/dist/session/run-audit.js +93 -7
  256. package/dist/session/run-audit.js.map +1 -1
  257. package/dist/session/session-threads.d.ts +49 -0
  258. package/dist/session/session-threads.d.ts.map +1 -0
  259. package/dist/session/session-threads.js +70 -0
  260. package/dist/session/session-threads.js.map +1 -0
  261. package/dist/session/source-control-tools.d.ts +19 -1
  262. package/dist/session/source-control-tools.d.ts.map +1 -1
  263. package/dist/session/source-control-tools.js +78 -11
  264. package/dist/session/source-control-tools.js.map +1 -1
  265. package/dist/session/thinking-default.d.ts.map +1 -1
  266. package/dist/session/thinking-default.js +3 -4
  267. package/dist/session/thinking-default.js.map +1 -1
  268. package/dist/session/thread-session-stamp.d.ts +21 -0
  269. package/dist/session/thread-session-stamp.d.ts.map +1 -0
  270. package/dist/session/thread-session-stamp.js +20 -0
  271. package/dist/session/thread-session-stamp.js.map +1 -0
  272. package/dist/session/thread-title-mirror.d.ts +1 -1
  273. package/dist/session/thread-title-mirror.d.ts.map +1 -1
  274. package/dist/session/thread-title-mirror.js +1 -1
  275. package/dist/session/thread-title-mirror.js.map +1 -1
  276. package/dist/session/work-item-repository.d.ts +1 -1
  277. package/dist/session/work-item-repository.d.ts.map +1 -1
  278. package/dist/session/work-item-repository.js.map +1 -1
  279. package/dist/skills/service.d.ts +6 -8
  280. package/dist/skills/service.d.ts.map +1 -1
  281. package/dist/skills/service.js +3 -3
  282. package/dist/skills/service.js.map +1 -1
  283. package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -1
  284. package/dist/storage/domains/audit/agent-audit.js +2 -2
  285. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  286. package/dist/storage/domains/audit/domain.js +1 -1
  287. package/dist/storage/domains/filesystem/base.d.ts +4 -0
  288. package/dist/storage/domains/filesystem/base.d.ts.map +1 -1
  289. package/dist/storage/domains/filesystem/base.js +5 -0
  290. package/dist/storage/domains/filesystem/base.js.map +1 -1
  291. package/dist/storage/domains/integrations/base.d.ts +8 -0
  292. package/dist/storage/domains/integrations/base.d.ts.map +1 -1
  293. package/dist/storage/domains/integrations/base.js +7 -0
  294. package/dist/storage/domains/integrations/base.js.map +1 -1
  295. package/dist/storage/domains/projects/base.d.ts +6 -0
  296. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  297. package/dist/storage/domains/projects/base.js +12 -0
  298. package/dist/storage/domains/projects/base.js.map +1 -1
  299. package/dist/storage/domains/source-control/base.d.ts +28 -0
  300. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  301. package/dist/storage/domains/source-control/base.js +48 -0
  302. package/dist/storage/domains/source-control/base.js.map +1 -1
  303. package/dist/storage/domains/source-control/inmemory.d.ts +8 -0
  304. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  305. package/dist/storage/domains/source-control/inmemory.js +13 -1
  306. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  307. package/dist/storage/domains/work-items/base.d.ts +79 -7
  308. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  309. package/dist/storage/domains/work-items/base.js +172 -30
  310. package/dist/storage/domains/work-items/base.js.map +1 -1
  311. package/dist/timing.d.ts +23 -0
  312. package/dist/timing.d.ts.map +1 -1
  313. package/dist/timing.js +85 -6
  314. package/dist/timing.js.map +1 -1
  315. package/dist/tools/annotations.d.ts +7 -0
  316. package/dist/tools/annotations.d.ts.map +1 -0
  317. package/dist/tools/annotations.js +102 -0
  318. package/dist/tools/annotations.js.map +1 -0
  319. package/dist/tools/approval-tools.d.ts +15 -0
  320. package/dist/tools/approval-tools.d.ts.map +1 -0
  321. package/dist/tools/approval-tools.js +79 -0
  322. package/dist/tools/approval-tools.js.map +1 -0
  323. package/dist/tools/deliver.d.ts +71 -0
  324. package/dist/tools/deliver.d.ts.map +1 -0
  325. package/dist/tools/deliver.js +129 -0
  326. package/dist/tools/deliver.js.map +1 -0
  327. package/dist/tools/index.d.ts +30 -0
  328. package/dist/tools/index.d.ts.map +1 -0
  329. package/dist/tools/index.js +36 -0
  330. package/dist/tools/index.js.map +1 -0
  331. package/dist/tools/messaging-tools.d.ts +28 -0
  332. package/dist/tools/messaging-tools.d.ts.map +1 -0
  333. package/dist/tools/messaging-tools.js +396 -0
  334. package/dist/tools/messaging-tools.js.map +1 -0
  335. package/dist/tools/permissions.d.ts +31 -0
  336. package/dist/tools/permissions.d.ts.map +1 -0
  337. package/dist/tools/permissions.js +48 -0
  338. package/dist/tools/permissions.js.map +1 -0
  339. package/dist/tools/read-tools.d.ts +30 -0
  340. package/dist/tools/read-tools.d.ts.map +1 -0
  341. package/dist/tools/read-tools.js +293 -0
  342. package/dist/tools/read-tools.js.map +1 -0
  343. package/dist/tools/reports.d.ts +174 -0
  344. package/dist/tools/reports.d.ts.map +1 -0
  345. package/dist/tools/reports.js +380 -0
  346. package/dist/tools/reports.js.map +1 -0
  347. package/dist/tools/run-watch.d.ts +46 -0
  348. package/dist/tools/run-watch.d.ts.map +1 -0
  349. package/dist/tools/run-watch.js +102 -0
  350. package/dist/tools/run-watch.js.map +1 -0
  351. package/dist/tools/scope.d.ts +54 -0
  352. package/dist/tools/scope.d.ts.map +1 -0
  353. package/dist/tools/scope.js +87 -0
  354. package/dist/tools/scope.js.map +1 -0
  355. package/dist/tools/session-tools.d.ts +43 -0
  356. package/dist/tools/session-tools.d.ts.map +1 -0
  357. package/dist/tools/session-tools.js +410 -0
  358. package/dist/tools/session-tools.js.map +1 -0
  359. package/dist/tools/sessions-state-processor.d.ts +41 -0
  360. package/dist/tools/sessions-state-processor.d.ts.map +1 -0
  361. package/dist/tools/sessions-state-processor.js +190 -0
  362. package/dist/tools/sessions-state-processor.js.map +1 -0
  363. package/dist/tools/side-sessions.d.ts +53 -0
  364. package/dist/tools/side-sessions.d.ts.map +1 -0
  365. package/dist/tools/side-sessions.js +83 -0
  366. package/dist/tools/side-sessions.js.map +1 -0
  367. package/dist/tools/work-item-tools.d.ts +28 -0
  368. package/dist/tools/work-item-tools.d.ts.map +1 -0
  369. package/dist/tools/work-item-tools.js +342 -0
  370. package/dist/tools/work-item-tools.js.map +1 -0
  371. package/dist/work-items/create-work-item.d.ts +55 -0
  372. package/dist/work-items/create-work-item.d.ts.map +1 -0
  373. package/dist/work-items/create-work-item.js +101 -0
  374. package/dist/work-items/create-work-item.js.map +1 -0
  375. package/dist/work-items/decision-actions.d.ts +42 -0
  376. package/dist/work-items/decision-actions.d.ts.map +1 -0
  377. package/dist/work-items/decision-actions.js +70 -0
  378. package/dist/work-items/decision-actions.js.map +1 -0
  379. package/dist/workspace.d.ts +1 -1
  380. package/dist/workspace.d.ts.map +1 -1
  381. package/dist/workspace.js +15 -7
  382. package/dist/workspace.js.map +1 -1
  383. package/factory-skills/factory-gitlab-rereview/SKILL.md +1 -1
  384. package/factory-skills/factory-gitlab-review/SKILL.md +1 -1
  385. package/factory-skills/factory-plan/SKILL.md +4 -4
  386. package/factory-skills/factory-rereview/SKILL.md +3 -1
  387. package/factory-skills/factory-review/SKILL.md +3 -1
  388. package/factory-skills/factory-self-review/SKILL.md +46 -0
  389. package/package.json +13 -13
@@ -1 +1 @@
1
- {"version":3,"file":"environment-state-processor.js","names":[],"sources":["../../src/session/environment-state-processor.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { ComputeStateSignalArgs, ComputeStateSignalResult, Processor } from '@mastra/core/processors';\nimport type { RequestContext } from '@mastra/core/request-context';\n\nconst STATE_ID = 'factory-environment';\n\nexport type EnvironmentRepositorySetupStatus = 'ok' | 'failed' | 'skipped';\n\n/** One repository of the booted environment as the agent is told about it: never a token or a clone URL. */\nexport interface SessionEnvironmentRepositoryState {\n slug: string;\n /** Absolute directory of the checkout under the workspace root. */\n dir: string;\n /** The branch the checkout ended on after boot; `null` when it stays detached. */\n branch: string | null;\n defaultBranch: string;\n position: number;\n /** `ok` ran or was already set up, `failed` exited non-zero, `skipped` has no setup command. */\n setupStatus: EnvironmentRepositorySetupStatus;\n /** The change request this session opened in the repository, once it has. */\n changeRequestUrl?: string;\n}\n\nexport interface SessionEnvironmentState {\n /** The workspace root, the session's working directory; repositories sit beneath it. */\n workingDirectory: string;\n repositories: SessionEnvironmentRepositoryState[];\n /** One free-form line the boot wants the agent to know (never a credential), rendered last. */\n note?: string;\n}\n\n// The boot records what it materialized, keyed by the factory session id (the\n// agent's resource id); the processor reads it on the next model step. Boot\n// runs on every sandbox start, so a replacement process re-learns the state\n// the first time the session's sandbox starts again.\nconst environments = new Map<string, { state: SessionEnvironmentState; teardown: SessionEnvironmentTeardown[] }>();\n\n/** A repository's teardown command and the directory it runs in; kept beside the state, never rendered. */\nexport interface SessionEnvironmentTeardown {\n slug: string;\n dir: string;\n command: string;\n}\n\nexport function recordSessionEnvironment(\n sessionId: string,\n state: SessionEnvironmentState,\n teardown: SessionEnvironmentTeardown[] = [],\n): void {\n environments.set(sessionId, { state, teardown });\n}\n\n/**\n * Record what a source-control tool did in one environment repository (the\n * branch it pushed, the change request it opened) so the next signal snapshot\n * tells the agent. A no-op when the session's environment is not recorded in\n * this process or the slug is not part of it.\n */\nexport function updateSessionEnvironmentRepository(\n sessionId: string,\n slug: string,\n patch: { branch?: string; changeRequestUrl?: string },\n): void {\n const entry = environments.get(sessionId);\n if (!entry) return;\n const wanted = slug.toLowerCase();\n const repositories = entry.state.repositories.map(repo => {\n if (repo.slug.toLowerCase() !== wanted) return repo;\n return {\n ...repo,\n ...(patch.branch !== undefined ? { branch: patch.branch } : {}),\n ...(patch.changeRequestUrl !== undefined ? { changeRequestUrl: patch.changeRequestUrl } : {}),\n };\n });\n entry.state = { ...entry.state, repositories };\n}\n\n/** Set or clear the environment's note; a no-op when the session's environment is not recorded here. */\nexport function setSessionEnvironmentNote(sessionId: string, note: string | null): void {\n const entry = environments.get(sessionId);\n if (!entry) return;\n const { note: _previous, ...rest } = entry.state;\n entry.state = note ? { ...rest, note } : rest;\n}\n\nexport function clearSessionEnvironment(sessionId: string): void {\n environments.delete(sessionId);\n}\n\nexport function peekSessionEnvironment(sessionId: string): SessionEnvironmentState | undefined {\n return environments.get(sessionId)?.state;\n}\n\n/** Teardown commands of the booted environment, in position order. */\nexport function peekSessionEnvironmentTeardown(sessionId: string): SessionEnvironmentTeardown[] {\n return environments.get(sessionId)?.teardown ?? [];\n}\n\nexport function __clearSessionEnvironmentsForTests(): void {\n environments.clear();\n}\n\nfunction escapeText(value: string): string {\n return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');\n}\n\n// The session id is the controller's resource id. Unlike factory runs, a user\n// session carries no `factoryProjectId` in its controller state, and the\n// environment is recorded for both, so only the resource id is required.\nfunction sessionIdFromContext(requestContext: RequestContext | undefined): string | null {\n if (!requestContext || typeof requestContext.get !== 'function') return null;\n const controller = requestContext.get('controller') as { resourceId?: unknown } | undefined;\n return typeof controller?.resourceId === 'string' && controller.resourceId ? controller.resourceId : null;\n}\n\nfunction environmentCacheKey(state: SessionEnvironmentState): string {\n return `environment:${createHash('sha256').update(JSON.stringify(state)).digest('hex').slice(0, 16)}`;\n}\n\n/**\n * Tells the agent which repositories its sandbox holds and where: the\n * workspace root is the session's working directory and every environment\n * repository is a directory beneath it. Snapshot mode only; a new snapshot\n * is emitted when the booted set changes (a start after an environment edit).\n */\nexport class FactoryEnvironmentStateProcessor implements Processor<'factory-environment'> {\n readonly id = STATE_ID;\n readonly stateId = STATE_ID;\n\n async computeStateSignal(args: ComputeStateSignalArgs): Promise<ComputeStateSignalResult> {\n const sessionId = sessionIdFromContext(args.requestContext);\n if (!sessionId) return;\n const state = environments.get(sessionId)?.state;\n if (!state) return;\n const cacheKey = environmentCacheKey(state);\n const hasBase = Boolean(args.lastSnapshot) && args.contextWindow.hasSnapshot;\n if (hasBase && (args.tracking?.currentCacheKey ?? args.lastSnapshot?.metadata?.state?.cacheKey) === cacheKey)\n return;\n const lines = state.repositories.map(\n repo =>\n `${repo.position}. ${escapeText(repo.slug)} at ${escapeText(repo.dir)} on ${escapeText(repo.branch ?? '(detached)')} (default ${escapeText(repo.defaultBranch)}, setup ${repo.setupStatus})` +\n (repo.changeRequestUrl ? `, change request ${escapeText(repo.changeRequestUrl)}` : ''),\n );\n const only = state.repositories.length === 1 ? state.repositories[0] : undefined;\n const contents =\n (only\n ? `Factory environment: 1 repository, ${escapeText(only.slug)} at ${escapeText(only.dir)}, your working directory.\\n`\n : `Factory environment: ${state.repositories.length} repositories under ${escapeText(state.workingDirectory)}, your working directory.\\n`) +\n lines.join('\\n') +\n (only ? '' : '\\nRun git and project commands inside the repository directory they belong to.') +\n (state.note ? `\\n${escapeText(state.note)}` : '');\n return {\n id: STATE_ID,\n cacheKey,\n mode: 'snapshot',\n tagName: 'factory-environment',\n contents,\n value: { environment: state },\n attributes: { workingDirectory: only?.dir ?? state.workingDirectory, repositories: state.repositories.length },\n metadata: { value: { environment: state } },\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"environment-state-processor.js","names":[],"sources":["../../src/session/environment-state-processor.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { ComputeStateSignalArgs, ComputeStateSignalResult, Processor } from '@mastra/core/processors';\nimport type { RequestContext } from '@mastra/core/request-context';\n\nconst STATE_ID = 'factory-environment';\n\nexport type EnvironmentRepositorySetupStatus = 'ok' | 'failed' | 'skipped';\n\n/** One repository of the booted environment as the agent is told about it: never a token or a clone URL. */\nexport interface SessionEnvironmentRepositoryState {\n slug: string;\n /** Absolute directory of the checkout under the workspace root. */\n dir: string;\n /** The branch the checkout ended on after boot; `null` when it stays detached. */\n branch: string | null;\n defaultBranch: string;\n position: number;\n /** `ok` ran or was already set up, `failed` exited non-zero, `skipped` has no setup command. */\n setupStatus: EnvironmentRepositorySetupStatus;\n /** The change request this session opened in the repository, once it has. */\n changeRequestUrl?: string;\n}\n\nexport interface SessionEnvironmentState {\n /** The workspace root, the session's working directory; repositories sit beneath it. */\n workingDirectory: string;\n repositories: SessionEnvironmentRepositoryState[];\n /** One free-form line the boot wants the agent to know (never a credential), rendered last. */\n note?: string;\n}\n\n// The boot records what it materialized, keyed by the factory session id (the\n// agent's resource id); the processor reads it on the next model step. Boot\n// runs on every sandbox start, so a replacement process re-learns the state\n// the first time the session's sandbox starts again.\nconst environments = new Map<string, { state: SessionEnvironmentState; teardown: SessionEnvironmentTeardown[] }>();\n\n/** A repository's teardown command and the directory it runs in; kept beside the state, never rendered. */\nexport interface SessionEnvironmentTeardown {\n slug: string;\n dir: string;\n command: string;\n}\n\nexport function recordSessionEnvironment(\n sessionId: string,\n state: SessionEnvironmentState,\n teardown: SessionEnvironmentTeardown[] = [],\n): void {\n environments.set(sessionId, { state, teardown });\n}\n\n/**\n * Record what a source-control tool did in one environment repository (the\n * branch it pushed, the change request it opened) so the next signal snapshot\n * tells the agent. A no-op when the session's environment is not recorded in\n * this process or the slug is not part of it.\n */\nexport function updateSessionEnvironmentRepository(\n sessionId: string,\n slug: string,\n patch: { branch?: string; changeRequestUrl?: string },\n): void {\n const entry = environments.get(sessionId);\n if (!entry) return;\n const wanted = slug.toLowerCase();\n const repositories = entry.state.repositories.map(repo => {\n if (repo.slug.toLowerCase() !== wanted) return repo;\n return {\n ...repo,\n ...(patch.branch !== undefined ? { branch: patch.branch } : {}),\n ...(patch.changeRequestUrl !== undefined ? { changeRequestUrl: patch.changeRequestUrl } : {}),\n };\n });\n entry.state = { ...entry.state, repositories };\n}\n\n/** Set or clear the environment's note; a no-op when the session's environment is not recorded here. */\nexport function setSessionEnvironmentNote(sessionId: string, note: string | null): void {\n const entry = environments.get(sessionId);\n if (!entry) return;\n const { note: _previous, ...rest } = entry.state;\n entry.state = note ? { ...rest, note } : rest;\n}\n\nexport function clearSessionEnvironment(sessionId: string): void {\n environments.delete(sessionId);\n}\n\nexport function peekSessionEnvironment(sessionId: string): SessionEnvironmentState | undefined {\n return environments.get(sessionId)?.state;\n}\n\n/** Teardown commands of the booted environment, in position order. */\nexport function peekSessionEnvironmentTeardown(sessionId: string): SessionEnvironmentTeardown[] {\n return environments.get(sessionId)?.teardown ?? [];\n}\n\nexport function __clearSessionEnvironmentsForTests(): void {\n environments.clear();\n}\n\nfunction escapeText(value: string): string {\n return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');\n}\n\n// Unlike factory runs, a user session carries no `factoryProjectId` in its\n// controller state, and the environment is recorded for both, so only the\n// session id is required.\nfunction sessionIdFromContext(requestContext: RequestContext | undefined): string | null {\n if (!requestContext || typeof requestContext.get !== 'function') return null;\n const controller = requestContext.get('controller') as { sessionId?: unknown } | undefined;\n return typeof controller?.sessionId === 'string' && controller.sessionId ? controller.sessionId : null;\n}\n\nfunction environmentCacheKey(state: SessionEnvironmentState): string {\n return `environment:${createHash('sha256').update(JSON.stringify(state)).digest('hex').slice(0, 16)}`;\n}\n\n/**\n * Tells the agent which repositories its sandbox holds and where: the\n * workspace root is the session's working directory and every environment\n * repository is a directory beneath it. Snapshot mode only; a new snapshot\n * is emitted when the booted set changes (a start after an environment edit).\n */\nexport class FactoryEnvironmentStateProcessor implements Processor<'factory-environment'> {\n readonly id = STATE_ID;\n readonly stateId = STATE_ID;\n\n async computeStateSignal(args: ComputeStateSignalArgs): Promise<ComputeStateSignalResult> {\n const sessionId = sessionIdFromContext(args.requestContext);\n if (!sessionId) return;\n const state = environments.get(sessionId)?.state;\n if (!state) return;\n const cacheKey = environmentCacheKey(state);\n const hasBase = Boolean(args.lastSnapshot) && args.contextWindow.hasSnapshot;\n if (hasBase && (args.tracking?.currentCacheKey ?? args.lastSnapshot?.metadata?.state?.cacheKey) === cacheKey)\n return;\n const lines = state.repositories.map(\n repo =>\n `${repo.position}. ${escapeText(repo.slug)} at ${escapeText(repo.dir)} on ${escapeText(repo.branch ?? '(detached)')} (default ${escapeText(repo.defaultBranch)}, setup ${repo.setupStatus})` +\n (repo.changeRequestUrl ? `, change request ${escapeText(repo.changeRequestUrl)}` : ''),\n );\n const only = state.repositories.length === 1 ? state.repositories[0] : undefined;\n const contents =\n (only\n ? `Factory environment: 1 repository, ${escapeText(only.slug)} at ${escapeText(only.dir)}, your working directory.\\n`\n : `Factory environment: ${state.repositories.length} repositories under ${escapeText(state.workingDirectory)}, your working directory.\\n`) +\n lines.join('\\n') +\n (only ? '' : '\\nRun git and project commands inside the repository directory they belong to.') +\n (state.note ? `\\n${escapeText(state.note)}` : '');\n return {\n id: STATE_ID,\n cacheKey,\n mode: 'snapshot',\n tagName: 'factory-environment',\n contents,\n value: { environment: state },\n attributes: { workingDirectory: only?.dir ?? state.workingDirectory, repositories: state.repositories.length },\n metadata: { value: { environment: state } },\n };\n }\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import type { MastraCodeState } from '@mastra/code-sdk/schema';
2
2
  import type { AgentController } from '@mastra/core/agent-controller';
3
3
  import type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';
4
4
  import type { EffectiveModelScope, FactoryProjectsStorage } from '../storage/domains/projects/base.js';
5
- import { type SourceControlSession, type SourceControlStorageHandle } from '../storage/domains/source-control/base.js';
5
+ import type { SourceControlSession, SourceControlSessionVisibility, SourceControlStorageHandle } from '../storage/domains/source-control/base.js';
6
6
  type FactorySession = Awaited<ReturnType<AgentController<MastraCodeState>['createSession']>>;
7
7
  export declare class FactorySourceControlConflictError extends Error {
8
8
  }
@@ -22,10 +22,19 @@ export declare function resolveFactorySessionModel(args: {
22
22
  export interface SourceControlSessionLookup {
23
23
  getBySessionId(sessionId: string): Promise<SourceControlSession | null>;
24
24
  getSourceControlBySessionId(sessionId: string): Promise<SourceControlStorageHandle | null>;
25
+ /** Side sessions started by `originSessionId`, across every provider. */
26
+ listByOrigin(args: {
27
+ orgId: string;
28
+ originSessionId: string;
29
+ }): Promise<SourceControlSession[]>;
25
30
  rename(args: {
26
31
  sessionId: string;
27
32
  title: string;
28
33
  }): Promise<void>;
34
+ setOriginMuted(args: {
35
+ sessionId: string;
36
+ muted: boolean;
37
+ }): Promise<void>;
29
38
  markFirstMessage(args: {
30
39
  sessionId: string;
31
40
  }): Promise<void>;
@@ -71,6 +80,15 @@ export interface EnsureFactorySourceSessionArgs {
71
80
  * run has an interactive user — e.g. the person who approved a proposed run.
72
81
  */
73
82
  attributeToUserId?: string;
83
+ /** A pre-generated session id, so a caller can name the branch after it. Defaults to a fresh UUID. */
84
+ sessionId?: string;
85
+ /** Defaults to 'org'. A side session copies its parent's visibility. */
86
+ visibility?: SourceControlSessionVisibility;
87
+ /** The session and thread that started this one as a side session. */
88
+ origin?: {
89
+ sessionId: string;
90
+ threadId: string;
91
+ };
74
92
  }
75
93
  export interface EnsuredFactorySourceSession {
76
94
  sessionId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"factory-session.d.ts","sourceRoot":"","sources":["../../src/session/factory-session.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"factory-session.d.ts","sourceRoot":"","sources":["../../src/session/factory-session.ts"],"names":[],"mappings":"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"}
@@ -49,7 +49,9 @@ function createSourceControlSessionLookup(sourceControls) {
49
49
  return {
50
50
  getBySessionId: async (sessionId) => (await resolve(sessionId))?.session ?? null,
51
51
  getSourceControlBySessionId: async (sessionId) => (await resolve(sessionId))?.sourceControl ?? null,
52
+ listByOrigin: async (args) => (await Promise.all(sourceControls.map((sourceControl) => sourceControl.sessions.listByOrigin(args)))).flat(),
52
53
  rename: async (args) => (await resolve(args.sessionId))?.sourceControl.sessions.rename(args),
54
+ setOriginMuted: async (args) => (await resolve(args.sessionId))?.sourceControl.sessions.setOriginMuted(args),
53
55
  markFirstMessage: async (args) => (await resolve(args.sessionId))?.sourceControl.sessions.markFirstMessage(args),
54
56
  markFirstMeaningfulExec: async (args) => (await resolve(args.sessionId))?.sourceControl.sessions.markFirstMeaningfulExec(args)
55
57
  };
@@ -164,14 +166,15 @@ async function ensureFactorySourceSession(args) {
164
166
  if (!resolved.found) throw new FactorySourceSessionResolutionError(resolved.reason);
165
167
  const userId = args.attributeToUserId ?? resolved.connectedByUserId;
166
168
  const session = await sourceControl.sessions.create({
167
- sessionId: globalThis.crypto.randomUUID(),
169
+ sessionId: args.sessionId ?? globalThis.crypto.randomUUID(),
168
170
  projectRepositoryId: resolved.projectRepositoryId,
169
171
  factoryProjectId,
170
172
  orgId,
171
173
  userId,
172
174
  branch,
173
175
  baseBranch: resolved.baseBranch,
174
- visibility: "org"
176
+ visibility: args.visibility ?? "org",
177
+ ...args.origin ? { origin: args.origin } : {}
175
178
  });
176
179
  return {
177
180
  sessionId: session.sessionId,
@@ -1 +1 @@
1
- {"version":3,"file":"factory-session.js","names":[],"sources":["../../src/session/factory-session.ts"],"sourcesContent":["import type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController } from '@mastra/core/agent-controller';\n\nimport { resolveTenantProviderDefaultModel } from '../routes/tenant-credentials.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { EffectiveModelScope, FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport {\n SourceControlConnectionNotFoundError,\n type SourceControlSession,\n type SourceControlStorageHandle,\n} from '../storage/domains/source-control/base.js';\nimport { resolveEnvironmentRepositories } from './environment-repositories.js';\nimport { seedSessionOrg } from './org-seed.js';\n\ntype FactorySession = Awaited<ReturnType<AgentController<MastraCodeState>['createSession']>>;\n\nexport class FactorySourceControlConflictError extends Error {}\n\n/** Resolve the model and exclusive credential scope for a Factory session. */\nexport async function resolveFactorySessionModel(args: {\n projects: FactoryProjectsStorage | undefined;\n modelDefaults: ModelDefaultsStorage | undefined;\n factoryProjectId: string | undefined;\n orgId: string;\n ownerUserId: string;\n /** A work-item run or an interactive session; a hybrid Factory resolves a different scope for each. */\n context: 'run' | 'session';\n}): Promise<{ modelScope: EffectiveModelScope; modelId?: string }> {\n if (!args.projects || !args.factoryProjectId) throw new Error('Factory model policy is unavailable.');\n\n const project = await args.projects.getById({ id: args.factoryProjectId });\n if (!project || project.orgId !== args.orgId) throw new Error('Factory model policy is unavailable.');\n if (effectiveModelScope(project.modelScope, args.context) === 'factory') {\n return { modelScope: 'factory', ...(project.defaultModelId ? { modelId: project.defaultModelId } : {}) };\n }\n\n try {\n const userDefault = await args.modelDefaults?.get({ orgId: args.orgId, userId: args.ownerUserId });\n return { modelScope: 'user', ...(userDefault?.modelId ? { modelId: userDefault.modelId } : {}) };\n } catch (error) {\n console.warn('[Factory] Owner default model lookup failed; using the mode default model', {\n orgId: args.orgId,\n ownerUserId: args.ownerUserId,\n error: error instanceof Error ? error.message : String(error),\n });\n return { modelScope: 'user' };\n }\n}\n\nexport interface SourceControlSessionLookup {\n getBySessionId(sessionId: string): Promise<SourceControlSession | null>;\n getSourceControlBySessionId(sessionId: string): Promise<SourceControlStorageHandle | null>;\n rename(args: { sessionId: string; title: string }): Promise<void>;\n markFirstMessage(args: { sessionId: string }): Promise<void>;\n markFirstMeaningfulExec(args: { sessionId: string }): Promise<void>;\n}\n\n/**\n * Read or update a session across every registered source-control partition.\n * Session ids are globally generated, so more than one match is invalid and\n * fails closed instead of mutating an arbitrary provider.\n */\nexport function createSourceControlSessionLookup(\n sourceControls: readonly SourceControlStorageHandle[],\n): SourceControlSessionLookup {\n const resolve = async (\n sessionId: string,\n ): Promise<{ sourceControl: SourceControlStorageHandle; session: SourceControlSession } | null> => {\n const matches = (\n await Promise.all(\n sourceControls.map(async sourceControl => ({\n sourceControl,\n session: await sourceControl.sessions.getBySessionId(sessionId),\n })),\n )\n ).filter(\n (match): match is { sourceControl: SourceControlStorageHandle; session: SourceControlSession } =>\n match.session !== null,\n );\n if (matches.length > 1) throw new Error('Factory session exists in multiple source-control providers.');\n return matches[0] ?? null;\n };\n\n return {\n getBySessionId: async sessionId => (await resolve(sessionId))?.session ?? null,\n getSourceControlBySessionId: async sessionId => (await resolve(sessionId))?.sourceControl ?? null,\n rename: async args => (await resolve(args.sessionId))?.sourceControl.sessions.rename(args),\n markFirstMessage: async args => (await resolve(args.sessionId))?.sourceControl.sessions.markFirstMessage(args),\n markFirstMeaningfulExec: async args =>\n (await resolve(args.sessionId))?.sourceControl.sessions.markFirstMeaningfulExec(args),\n };\n}\n\n/**\n * Resolve the source-control provider from durable Factory repository links,\n * never from the issue tracker that happened to create the work item.\n */\nexport async function resolveFactorySourceControl(args: {\n sourceControls: readonly SourceControlStorageHandle[];\n orgId: string;\n factoryProjectId: string;\n sessionId?: string;\n}): Promise<SourceControlStorageHandle | undefined> {\n if (args.sessionId) {\n const sessionMatches = (\n await Promise.all(\n args.sourceControls.map(async sourceControl => ({\n sourceControl,\n session: await sourceControl.sessions.getBySessionId(args.sessionId!),\n })),\n )\n ).filter(match => match.session !== null);\n if (sessionMatches.length > 1) throw new Error('Factory session exists in multiple source-control providers.');\n if (sessionMatches[0]) return sessionMatches[0].sourceControl;\n }\n\n const linked = [];\n for (const sourceControl of args.sourceControls) {\n const connections = await sourceControl.connections.list({\n orgId: args.orgId,\n factoryProjectId: args.factoryProjectId,\n });\n let hasLinkedRepository = false;\n for (const connection of connections) {\n try {\n if (\n (await sourceControl.projectRepositories.list({ orgId: args.orgId, connectionId: connection.id })).length > 0\n ) {\n hasLinkedRepository = true;\n break;\n }\n } catch (error) {\n if (!(error instanceof SourceControlConnectionNotFoundError)) throw error;\n }\n }\n if (hasLinkedRepository) linked.push(sourceControl);\n }\n if (linked.length > 1)\n throw new FactorySourceControlConflictError(\n 'Factory project has repositories linked through multiple source-control providers.',\n );\n return linked[0];\n}\n\nexport interface EnsureFactorySourceSessionArgs {\n /**\n * Storage handle of the integration that owns source control. Nothing here is\n * provider-specific: the connection is matched by the handle's own\n * `integrationId`, so GitHub, Slack-on-behalf-of-GitHub, or any future owner\n * all resolve through the same traversal.\n */\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n factoryProjectId: string;\n branch: string;\n /**\n * The repository link the session is filed under: the one a work item\n * targets. Defaults to the factory's position-1 environment link (D1), for\n * callers where nothing names a repository (chat threads, user sessions).\n */\n projectRepositoryId?: string;\n /**\n * Attribute the run to this user instead of the repo connector. Set when the\n * run has an interactive user — e.g. the person who approved a proposed run.\n */\n attributeToUserId?: string;\n}\n\nexport interface EnsuredFactorySourceSession {\n sessionId: string;\n userId: string;\n projectRepositoryId: string;\n branch: string;\n baseBranch: string;\n}\n\nexport class FactorySourceSessionResolutionError extends Error {\n constructor(readonly reason: 'connection' | 'repository') {\n super(\n reason === 'connection'\n ? 'Factory source-control connection not found.'\n : 'Factory source-control repository not found.',\n );\n this.name = 'FactorySourceSessionResolutionError';\n }\n}\n\nexport interface ResolvedFactorySourceRepository {\n projectRepositoryId: string;\n /** The repository's pinned branch, else its default branch. */\n baseBranch: string;\n /** Who connected the repository. The attribution for runs with no interactive user. */\n connectedByUserId: string;\n}\n\n/**\n * Outcome of {@link resolvePrimaryEnvironmentRepository}. A miss carries which\n * step failed: callers differ on whether that is an error (an autonomous run\n * cannot proceed) or a routine fallback (a chat integration drops to a\n * chat-only session), and the two steps fail for different reasons worth\n * reporting apart.\n */\nexport type FactorySourceRepositoryResult =\n | ({ found: true } & ResolvedFactorySourceRepository)\n | { found: false; reason: 'connection' | 'repository' };\n\n/**\n * The repository link a new session is filed under. By default the position-1\n * `inEnvironment` link of the factory (decision D1); a work item that targets\n * a repository names its link through `projectRepositoryId`, which must be an\n * environment link of the same factory. Session start no longer picks a\n * repository to work in, the sandbox holds every environment repository. The\n * PR tools default to this link, and subscriptions, audit and the work item's\n * repository check still read it; moving them to the per-repository\n * `sessionRepositories` rows is a follow-up.\n *\n * The owner is whichever integration owns source control, matched by the\n * handle's own `integrationId` — nothing here is provider-specific.\n */\nexport async function resolvePrimaryEnvironmentRepository(args: {\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n factoryProjectId: string;\n projectRepositoryId?: string;\n}): Promise<FactorySourceRepositoryResult> {\n const { sourceControl, orgId, factoryProjectId, projectRepositoryId } = args;\n const connections = await sourceControl.connections.list({ orgId, factoryProjectId });\n if (!connections.some(candidate => candidate.integrationId === sourceControl.integrationId)) {\n return { found: false, reason: 'connection' };\n }\n const repositories = await resolveEnvironmentRepositories({ sourceControl, orgId, factoryProjectId });\n const primary = repositories.find(\n candidate => projectRepositoryId === undefined || candidate.link.id === projectRepositoryId,\n );\n if (!primary) return { found: false, reason: 'repository' };\n return {\n found: true,\n projectRepositoryId: primary.link.id,\n baseBranch: primary.link.branch ?? primary.repository.defaultBranch,\n connectedByUserId: primary.connection.createdByUserId,\n };\n}\n\n/**\n * Create the source-control session a repo-backed factory run needs.\n *\n * `FactoryStartCoordinator.prepare` requires this record to already exist —\n * `resolveSourceSession` throws `Factory session not found` otherwise — so every\n * autonomous entry point has to produce one before it can start a run. This is\n * that step, in one place: the owner's connection on the factory project, the\n * work item's repository link (else the position-1 environment link, D1) as\n * the session's link, and a session on the requested branch with that\n * repository's pinned or default branch as the base. The sandbox itself boots\n * every environment repository.\n *\n * The run is attributed to `attributeToUserId` when the caller has an\n * interactive user (e.g. the approver of a proposed run), and otherwise falls\n * back to whoever connected the repository (`connection.createdByUserId`),\n * because a genuinely autonomous run has no interactive user of its own.\n */\nexport async function ensureFactorySourceSession(\n args: EnsureFactorySourceSessionArgs,\n): Promise<EnsuredFactorySourceSession> {\n const { sourceControl, orgId, factoryProjectId, branch, projectRepositoryId } = args;\n\n const resolved = await resolvePrimaryEnvironmentRepository({\n sourceControl,\n orgId,\n factoryProjectId,\n projectRepositoryId,\n });\n if (!resolved.found) throw new FactorySourceSessionResolutionError(resolved.reason);\n\n const userId = args.attributeToUserId ?? resolved.connectedByUserId;\n const session = await sourceControl.sessions.create({\n sessionId: globalThis.crypto.randomUUID(),\n projectRepositoryId: resolved.projectRepositoryId,\n factoryProjectId,\n orgId,\n userId,\n branch,\n baseBranch: resolved.baseBranch,\n visibility: 'org',\n });\n return {\n sessionId: session.sessionId,\n userId,\n projectRepositoryId: resolved.projectRepositoryId,\n branch: session.branch,\n baseBranch: resolved.baseBranch,\n };\n}\n\nexport interface HydrateFactorySessionArgs {\n orgId: string;\n modelScope: EffectiveModelScope;\n ownerUserId: string;\n /**\n * The model selected by the Factory scope. Without it the provider default of a connected\n * provider is applied when the SDK mode default has no credential, else the mode default.\n */\n defaultModelId?: string;\n}\n\n/**\n * Apply a factory project's main model to a session on every run.\n *\n * The model switch is best-effort. A retired model id must not sink a run that\n * is otherwise ready, and the reason is logged. Observational-memory settings are resolved from the\n * authoritative row per invocation before memory processors run.\n */\nexport async function hydrateFactorySession(\n session: FactorySession,\n args: HydrateFactorySessionArgs,\n): Promise<string | undefined> {\n // The org rung knowledge curation scopes on. Seeded first so it lands even if\n // a later best-effort step fails; an empty org marks the session unresolved.\n await seedSessionOrg(session, args.orgId);\n await session.state.set({\n factoryModelScope: args.modelScope,\n factoryOwnerUserId: args.ownerUserId,\n });\n\n let selectedModelId = args.defaultModelId;\n if (!selectedModelId) {\n const modeModelId = session.mode.resolve().defaultModelId;\n const providerModelId = await resolveTenantProviderDefaultModel(\n { orgId: args.orgId, userId: args.ownerUserId, credentialScope: args.modelScope },\n modeModelId,\n ).catch(error => {\n console.warn('[Factory Start] Failed to resolve a provider default model', {\n error: error instanceof Error ? error.message : String(error),\n });\n return undefined;\n });\n selectedModelId = providerModelId ?? modeModelId;\n }\n if (!selectedModelId) return undefined;\n\n let switched = true;\n try {\n await session.model.switch(selectedModelId);\n } catch (error) {\n switched = false;\n console.warn('[Factory Start] Failed to apply scoped default model', {\n modelId: selectedModelId,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n // Subagents otherwise keep the server-wide settings (or the SDK's built-in\n // default), which may name a provider this factory has no credentials for.\n // Each role is independent, so one failure doesn't strand the others.\n for (const agentType of ['explore', 'plan', 'execute']) {\n try {\n await session.subagents.model.set({ modelId: selectedModelId, agentType });\n } catch (error) {\n console.warn('[Factory Start] Failed to apply scoped default subagent model', {\n agentType,\n modelId: selectedModelId,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n // Callers pin this id to the thread, so only report a model the session actually runs on.\n return switched ? selectedModelId : undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"factory-session.js","names":[],"sources":["../../src/session/factory-session.ts"],"sourcesContent":["import type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController } from '@mastra/core/agent-controller';\n\nimport { resolveTenantProviderDefaultModel } from '../routes/tenant-credentials.js';\nimport type { ModelDefaultsStorage } from '../storage/domains/model-defaults/base.js';\nimport { effectiveModelScope } from '../storage/domains/projects/base.js';\nimport type { EffectiveModelScope, FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport { SourceControlConnectionNotFoundError } from '../storage/domains/source-control/base.js';\nimport type {\n SourceControlSession,\n SourceControlSessionVisibility,\n SourceControlStorageHandle,\n} from '../storage/domains/source-control/base.js';\nimport { resolveEnvironmentRepositories } from './environment-repositories.js';\nimport { seedSessionOrg } from './org-seed.js';\n\ntype FactorySession = Awaited<ReturnType<AgentController<MastraCodeState>['createSession']>>;\n\nexport class FactorySourceControlConflictError extends Error {}\n\n/** Resolve the model and exclusive credential scope for a Factory session. */\nexport async function resolveFactorySessionModel(args: {\n projects: FactoryProjectsStorage | undefined;\n modelDefaults: ModelDefaultsStorage | undefined;\n factoryProjectId: string | undefined;\n orgId: string;\n ownerUserId: string;\n /** A work-item run or an interactive session; a hybrid Factory resolves a different scope for each. */\n context: 'run' | 'session';\n}): Promise<{ modelScope: EffectiveModelScope; modelId?: string }> {\n if (!args.projects || !args.factoryProjectId) throw new Error('Factory model policy is unavailable.');\n\n const project = await args.projects.getById({ id: args.factoryProjectId });\n if (!project || project.orgId !== args.orgId) throw new Error('Factory model policy is unavailable.');\n if (effectiveModelScope(project.modelScope, args.context) === 'factory') {\n return { modelScope: 'factory', ...(project.defaultModelId ? { modelId: project.defaultModelId } : {}) };\n }\n\n try {\n const userDefault = await args.modelDefaults?.get({ orgId: args.orgId, userId: args.ownerUserId });\n return { modelScope: 'user', ...(userDefault?.modelId ? { modelId: userDefault.modelId } : {}) };\n } catch (error) {\n console.warn('[Factory] Owner default model lookup failed; using the mode default model', {\n orgId: args.orgId,\n ownerUserId: args.ownerUserId,\n error: error instanceof Error ? error.message : String(error),\n });\n return { modelScope: 'user' };\n }\n}\n\nexport interface SourceControlSessionLookup {\n getBySessionId(sessionId: string): Promise<SourceControlSession | null>;\n getSourceControlBySessionId(sessionId: string): Promise<SourceControlStorageHandle | null>;\n /** Side sessions started by `originSessionId`, across every provider. */\n listByOrigin(args: { orgId: string; originSessionId: string }): Promise<SourceControlSession[]>;\n rename(args: { sessionId: string; title: string }): Promise<void>;\n setOriginMuted(args: { sessionId: string; muted: boolean }): Promise<void>;\n markFirstMessage(args: { sessionId: string }): Promise<void>;\n markFirstMeaningfulExec(args: { sessionId: string }): Promise<void>;\n}\n\n/**\n * Read or update a session across every registered source-control partition.\n * Session ids are globally generated, so more than one match is invalid and\n * fails closed instead of mutating an arbitrary provider.\n */\nexport function createSourceControlSessionLookup(\n sourceControls: readonly SourceControlStorageHandle[],\n): SourceControlSessionLookup {\n const resolve = async (\n sessionId: string,\n ): Promise<{ sourceControl: SourceControlStorageHandle; session: SourceControlSession } | null> => {\n const matches = (\n await Promise.all(\n sourceControls.map(async sourceControl => ({\n sourceControl,\n session: await sourceControl.sessions.getBySessionId(sessionId),\n })),\n )\n ).filter(\n (match): match is { sourceControl: SourceControlStorageHandle; session: SourceControlSession } =>\n match.session !== null,\n );\n if (matches.length > 1) throw new Error('Factory session exists in multiple source-control providers.');\n return matches[0] ?? null;\n };\n\n return {\n getBySessionId: async sessionId => (await resolve(sessionId))?.session ?? null,\n getSourceControlBySessionId: async sessionId => (await resolve(sessionId))?.sourceControl ?? null,\n listByOrigin: async args =>\n (await Promise.all(sourceControls.map(sourceControl => sourceControl.sessions.listByOrigin(args)))).flat(),\n rename: async args => (await resolve(args.sessionId))?.sourceControl.sessions.rename(args),\n setOriginMuted: async args => (await resolve(args.sessionId))?.sourceControl.sessions.setOriginMuted(args),\n markFirstMessage: async args => (await resolve(args.sessionId))?.sourceControl.sessions.markFirstMessage(args),\n markFirstMeaningfulExec: async args =>\n (await resolve(args.sessionId))?.sourceControl.sessions.markFirstMeaningfulExec(args),\n };\n}\n\n/**\n * Resolve the source-control provider from durable Factory repository links,\n * never from the issue tracker that happened to create the work item.\n */\nexport async function resolveFactorySourceControl(args: {\n sourceControls: readonly SourceControlStorageHandle[];\n orgId: string;\n factoryProjectId: string;\n sessionId?: string;\n}): Promise<SourceControlStorageHandle | undefined> {\n if (args.sessionId) {\n const sessionMatches = (\n await Promise.all(\n args.sourceControls.map(async sourceControl => ({\n sourceControl,\n session: await sourceControl.sessions.getBySessionId(args.sessionId!),\n })),\n )\n ).filter(match => match.session !== null);\n if (sessionMatches.length > 1) throw new Error('Factory session exists in multiple source-control providers.');\n if (sessionMatches[0]) return sessionMatches[0].sourceControl;\n }\n\n const linked = [];\n for (const sourceControl of args.sourceControls) {\n const connections = await sourceControl.connections.list({\n orgId: args.orgId,\n factoryProjectId: args.factoryProjectId,\n });\n let hasLinkedRepository = false;\n for (const connection of connections) {\n try {\n if (\n (await sourceControl.projectRepositories.list({ orgId: args.orgId, connectionId: connection.id })).length > 0\n ) {\n hasLinkedRepository = true;\n break;\n }\n } catch (error) {\n if (!(error instanceof SourceControlConnectionNotFoundError)) throw error;\n }\n }\n if (hasLinkedRepository) linked.push(sourceControl);\n }\n if (linked.length > 1)\n throw new FactorySourceControlConflictError(\n 'Factory project has repositories linked through multiple source-control providers.',\n );\n return linked[0];\n}\n\nexport interface EnsureFactorySourceSessionArgs {\n /**\n * Storage handle of the integration that owns source control. Nothing here is\n * provider-specific: the connection is matched by the handle's own\n * `integrationId`, so GitHub, Slack-on-behalf-of-GitHub, or any future owner\n * all resolve through the same traversal.\n */\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n factoryProjectId: string;\n branch: string;\n /**\n * The repository link the session is filed under: the one a work item\n * targets. Defaults to the factory's position-1 environment link (D1), for\n * callers where nothing names a repository (chat threads, user sessions).\n */\n projectRepositoryId?: string;\n /**\n * Attribute the run to this user instead of the repo connector. Set when the\n * run has an interactive user — e.g. the person who approved a proposed run.\n */\n attributeToUserId?: string;\n /** A pre-generated session id, so a caller can name the branch after it. Defaults to a fresh UUID. */\n sessionId?: string;\n /** Defaults to 'org'. A side session copies its parent's visibility. */\n visibility?: SourceControlSessionVisibility;\n /** The session and thread that started this one as a side session. */\n origin?: { sessionId: string; threadId: string };\n}\n\nexport interface EnsuredFactorySourceSession {\n sessionId: string;\n userId: string;\n projectRepositoryId: string;\n branch: string;\n baseBranch: string;\n}\n\nexport class FactorySourceSessionResolutionError extends Error {\n constructor(readonly reason: 'connection' | 'repository') {\n super(\n reason === 'connection'\n ? 'Factory source-control connection not found.'\n : 'Factory source-control repository not found.',\n );\n this.name = 'FactorySourceSessionResolutionError';\n }\n}\n\nexport interface ResolvedFactorySourceRepository {\n projectRepositoryId: string;\n /** The repository's pinned branch, else its default branch. */\n baseBranch: string;\n /** Who connected the repository. The attribution for runs with no interactive user. */\n connectedByUserId: string;\n}\n\n/**\n * Outcome of {@link resolvePrimaryEnvironmentRepository}. A miss carries which\n * step failed: callers differ on whether that is an error (an autonomous run\n * cannot proceed) or a routine fallback (a chat integration drops to a\n * chat-only session), and the two steps fail for different reasons worth\n * reporting apart.\n */\nexport type FactorySourceRepositoryResult =\n | ({ found: true } & ResolvedFactorySourceRepository)\n | { found: false; reason: 'connection' | 'repository' };\n\n/**\n * The repository link a new session is filed under. By default the position-1\n * `inEnvironment` link of the factory (decision D1); a work item that targets\n * a repository names its link through `projectRepositoryId`, which must be an\n * environment link of the same factory. Session start no longer picks a\n * repository to work in, the sandbox holds every environment repository. The\n * PR tools default to this link, and subscriptions, audit and the work item's\n * repository check still read it; moving them to the per-repository\n * `sessionRepositories` rows is a follow-up.\n *\n * The owner is whichever integration owns source control, matched by the\n * handle's own `integrationId` — nothing here is provider-specific.\n */\nexport async function resolvePrimaryEnvironmentRepository(args: {\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n factoryProjectId: string;\n projectRepositoryId?: string;\n}): Promise<FactorySourceRepositoryResult> {\n const { sourceControl, orgId, factoryProjectId, projectRepositoryId } = args;\n const connections = await sourceControl.connections.list({ orgId, factoryProjectId });\n if (!connections.some(candidate => candidate.integrationId === sourceControl.integrationId)) {\n return { found: false, reason: 'connection' };\n }\n const repositories = await resolveEnvironmentRepositories({ sourceControl, orgId, factoryProjectId });\n const primary = repositories.find(\n candidate => projectRepositoryId === undefined || candidate.link.id === projectRepositoryId,\n );\n if (!primary) return { found: false, reason: 'repository' };\n return {\n found: true,\n projectRepositoryId: primary.link.id,\n baseBranch: primary.link.branch ?? primary.repository.defaultBranch,\n connectedByUserId: primary.connection.createdByUserId,\n };\n}\n\n/**\n * Create the source-control session a repo-backed factory run needs.\n *\n * `FactoryStartCoordinator.prepare` requires this record to already exist —\n * `resolveSourceSession` throws `Factory session not found` otherwise — so every\n * autonomous entry point has to produce one before it can start a run. This is\n * that step, in one place: the owner's connection on the factory project, the\n * work item's repository link (else the position-1 environment link, D1) as\n * the session's link, and a session on the requested branch with that\n * repository's pinned or default branch as the base. The sandbox itself boots\n * every environment repository.\n *\n * The run is attributed to `attributeToUserId` when the caller has an\n * interactive user (e.g. the approver of a proposed run), and otherwise falls\n * back to whoever connected the repository (`connection.createdByUserId`),\n * because a genuinely autonomous run has no interactive user of its own.\n */\nexport async function ensureFactorySourceSession(\n args: EnsureFactorySourceSessionArgs,\n): Promise<EnsuredFactorySourceSession> {\n const { sourceControl, orgId, factoryProjectId, branch, projectRepositoryId } = args;\n\n const resolved = await resolvePrimaryEnvironmentRepository({\n sourceControl,\n orgId,\n factoryProjectId,\n projectRepositoryId,\n });\n if (!resolved.found) throw new FactorySourceSessionResolutionError(resolved.reason);\n\n const userId = args.attributeToUserId ?? resolved.connectedByUserId;\n const session = await sourceControl.sessions.create({\n sessionId: args.sessionId ?? globalThis.crypto.randomUUID(),\n projectRepositoryId: resolved.projectRepositoryId,\n factoryProjectId,\n orgId,\n userId,\n branch,\n baseBranch: resolved.baseBranch,\n visibility: args.visibility ?? 'org',\n ...(args.origin ? { origin: args.origin } : {}),\n });\n return {\n sessionId: session.sessionId,\n userId,\n projectRepositoryId: resolved.projectRepositoryId,\n branch: session.branch,\n baseBranch: resolved.baseBranch,\n };\n}\n\nexport interface HydrateFactorySessionArgs {\n orgId: string;\n modelScope: EffectiveModelScope;\n ownerUserId: string;\n /**\n * The model selected by the Factory scope. Without it the provider default of a connected\n * provider is applied when the SDK mode default has no credential, else the mode default.\n */\n defaultModelId?: string;\n}\n\n/**\n * Apply a factory project's main model to a session on every run.\n *\n * The model switch is best-effort. A retired model id must not sink a run that\n * is otherwise ready, and the reason is logged. Observational-memory settings are resolved from the\n * authoritative row per invocation before memory processors run.\n */\nexport async function hydrateFactorySession(\n session: FactorySession,\n args: HydrateFactorySessionArgs,\n): Promise<string | undefined> {\n // The org rung knowledge curation scopes on. Seeded first so it lands even if\n // a later best-effort step fails; an empty org marks the session unresolved.\n await seedSessionOrg(session, args.orgId);\n await session.state.set({\n factoryModelScope: args.modelScope,\n factoryOwnerUserId: args.ownerUserId,\n });\n\n let selectedModelId = args.defaultModelId;\n if (!selectedModelId) {\n const modeModelId = session.mode.resolve().defaultModelId;\n const providerModelId = await resolveTenantProviderDefaultModel(\n { orgId: args.orgId, userId: args.ownerUserId, credentialScope: args.modelScope },\n modeModelId,\n ).catch(error => {\n console.warn('[Factory Start] Failed to resolve a provider default model', {\n error: error instanceof Error ? error.message : String(error),\n });\n return undefined;\n });\n selectedModelId = providerModelId ?? modeModelId;\n }\n if (!selectedModelId) return undefined;\n\n let switched = true;\n try {\n await session.model.switch(selectedModelId);\n } catch (error) {\n switched = false;\n console.warn('[Factory Start] Failed to apply scoped default model', {\n modelId: selectedModelId,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n // Subagents otherwise keep the server-wide settings (or the SDK's built-in\n // default), which may name a provider this factory has no credentials for.\n // Each role is independent, so one failure doesn't strand the others.\n for (const agentType of ['explore', 'plan', 'execute']) {\n try {\n await session.subagents.model.set({ modelId: selectedModelId, agentType });\n } catch (error) {\n console.warn('[Factory Start] Failed to apply scoped default subagent model', {\n agentType,\n modelId: selectedModelId,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n // Callers pin this id to the thread, so only report a model the session actually runs on.\n return switched ? selectedModelId : undefined;\n}\n"],"mappings":"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"}
@@ -17,7 +17,7 @@ export declare const ROOT_FILES_LIST_COMMAND: string;
17
17
  export declare function touchStamp(date: Date): string;
18
18
  export interface FilesystemCaptureSession {
19
19
  readonly identity: {
20
- getResourceId(): string;
20
+ getId(): string;
21
21
  };
22
22
  readonly thread: {
23
23
  requireId(): string;
@@ -36,7 +36,7 @@ export interface FilesystemCaptureDependencies {
36
36
  }
37
37
  export declare function parseFilesystemCaptureFiles(output: string): FilesystemFile[];
38
38
  export declare function captureSessionFilesystem(session: FilesystemCaptureSession, { filesystem, sourceControl }: FilesystemCaptureDependencies, ids?: {
39
- resourceId: string;
39
+ sessionId: string;
40
40
  threadId: string;
41
41
  }): Promise<void>;
42
42
  /**
@@ -45,6 +45,6 @@ export declare function captureSessionFilesystem(session: FilesystemCaptureSessi
45
45
  * never blocked by the execs' worst-case 30s timeouts. Never rejects: the
46
46
  * capture body is fully try/catch contained.
47
47
  */
48
- export declare function waitForPendingFilesystemCapture(resourceId: string, timeoutMs?: number): Promise<void>;
48
+ export declare function waitForPendingFilesystemCapture(sessionId: string, timeoutMs?: number): Promise<void>;
49
49
  export declare function observeSessionFilesystem(session: FilesystemCaptureSession, dependencies: FilesystemCaptureDependencies): () => void;
50
50
  //# sourceMappingURL=filesystem-capture.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"filesystem-capture.d.ts","sourceRoot":"","sources":["../../src/session/filesystem-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACzG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAI/D,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC/F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAmB5F;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,QAIzB,CAAC;AAEZ,4CAA4C;AAC5C,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAM7C;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,QAAQ,CAAC,MAAM,EAAE;QAAE,SAAS,IAAI,MAAM,CAAA;KAAE,CAAC;IACzC,YAAY,IAAI;QAAE,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;IACnF,gBAAgB,CAAC,QAAQ,EAAE,6BAA6B,GAAG,MAAM,IAAI,CAAC;IACtE,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;IACpD,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,CAAC;KAC1E,CAAC;CACH;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAS5E;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,wBAAwB,EACjC,EAAE,UAAU,EAAE,aAAa,EAAE,EAAE,6BAA6B,EAC5D,GAAG,CAAC,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC7C,OAAO,CAAC,IAAI,CAAC,CAkHf;AAYD;;;;;GAKG;AACH,wBAAsB,+BAA+B,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB3G;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,wBAAwB,EACjC,YAAY,EAAE,6BAA6B,GAC1C,MAAM,IAAI,CA2FZ"}
1
+ {"version":3,"file":"filesystem-capture.d.ts","sourceRoot":"","sources":["../../src/session/filesystem-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACzG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAI/D,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC/F,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAmB5F;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,QAIzB,CAAC;AAEZ,4CAA4C;AAC5C,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAM7C;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE;QAAE,KAAK,IAAI,MAAM,CAAA;KAAE,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE;QAAE,SAAS,IAAI,MAAM,CAAA;KAAE,CAAC;IACzC,YAAY,IAAI;QAAE,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;IACnF,gBAAgB,CAAC,QAAQ,EAAE,6BAA6B,GAAG,MAAM,IAAI,CAAC;IACtE,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,IAAI,CAAC,iBAAiB,EAAE,cAAc,CAAC,CAAC;IACpD,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,gBAAgB,CAAC,CAAC;KAC1E,CAAC;CACH;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,EAAE,CAS5E;AAED,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,wBAAwB,EACjC,EAAE,UAAU,EAAE,aAAa,EAAE,EAAE,6BAA6B,EAC5D,GAAG,CAAC,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC5C,OAAO,CAAC,IAAI,CAAC,CAoHf;AAWD;;;;;GAKG;AACH,wBAAsB,+BAA+B,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB1G;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,wBAAwB,EACjC,YAAY,EAAE,6BAA6B,GAC1C,MAAM,IAAI,CAwFZ"}
@@ -48,9 +48,9 @@ function parseFilesystemCaptureFiles(output) {
48
48
  }
49
49
  async function captureSessionFilesystem(session, { filesystem, sourceControl }, ids) {
50
50
  try {
51
- const resourceId = ids?.resourceId ?? session.identity.getResourceId();
51
+ const sessionId = ids?.sessionId ?? session.identity.getId();
52
52
  const threadId = ids?.threadId ?? session.thread.requireId();
53
- const sourceSession = await sourceControl.sessions.getBySessionId(resourceId);
53
+ const sourceSession = await sourceControl.sessions.getBySessionId(sessionId);
54
54
  const sandbox = session.getWorkspace()?.sandbox;
55
55
  if (!sourceSession || !sandbox?.executeCommand) return;
56
56
  const entry = peekSessionSandbox(sourceSession.id);
@@ -120,7 +120,7 @@ async function captureSessionFilesystem(session, { filesystem, sourceControl },
120
120
  }
121
121
  }
122
122
  await filesystem.replaceFiles({
123
- resourceId,
123
+ resourceId: sessionId,
124
124
  threadId,
125
125
  files: [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path))
126
126
  });
@@ -129,11 +129,10 @@ async function captureSessionFilesystem(session, { filesystem, sourceControl },
129
129
  }
130
130
  }
131
131
  /**
132
- * In-flight capture chains keyed by session resource id. Serves two purposes:
133
- * readers of the persisted file listing (the /web/workspace routes) await the
134
- * capture for the turn that just ended instead of racing it, and captures for
135
- * the same resource stay sequential (last write wins) even when multiple
136
- * scoped sessions observe the same resource id. In-process only: the factory
132
+ * In-flight capture chains keyed by session id. Serves two purposes: readers
133
+ * of the persisted file listing (the /web/workspace routes) await the capture
134
+ * for the turn that just ended instead of racing it, and captures for the same
135
+ * session stay sequential (last write wins). In-process only: the factory
137
136
  * server that runs the session controller also serves those routes.
138
137
  */
139
138
  const pendingCaptures = /* @__PURE__ */ new Map();
@@ -143,8 +142,8 @@ const pendingCaptures = /* @__PURE__ */ new Map();
143
142
  * never blocked by the execs' worst-case 30s timeouts. Never rejects: the
144
143
  * capture body is fully try/catch contained.
145
144
  */
146
- async function waitForPendingFilesystemCapture(resourceId, timeoutMs = 1e4) {
147
- let pending = pendingCaptures.get(resourceId);
145
+ async function waitForPendingFilesystemCapture(sessionId, timeoutMs = 1e4) {
146
+ let pending = pendingCaptures.get(sessionId);
148
147
  if (!pending) return;
149
148
  let timer;
150
149
  let timedOut = false;
@@ -157,7 +156,7 @@ async function waitForPendingFilesystemCapture(resourceId, timeoutMs = 1e4) {
157
156
  try {
158
157
  while (pending && !timedOut) {
159
158
  await Promise.race([pending, deadline]);
160
- const next = pendingCaptures.get(resourceId);
159
+ const next = pendingCaptures.get(sessionId);
161
160
  pending = next === pending ? void 0 : next;
162
161
  }
163
162
  } finally {
@@ -197,9 +196,9 @@ function observeSessionFilesystem(session, dependencies) {
197
196
  const unsubscribeEnd = session.onBeforeAgentEnd(() => {
198
197
  if (!workspaceTouched) return;
199
198
  workspaceTouched = false;
200
- let resourceId;
199
+ let sessionId;
201
200
  try {
202
- resourceId = session.identity.getResourceId();
201
+ sessionId = session.identity.getId();
203
202
  } catch {
204
203
  fallbackChain = fallbackChain.then(() => captureSessionFilesystem(session, dependencies));
205
204
  return;
@@ -211,13 +210,13 @@ function observeSessionFilesystem(session, dependencies) {
211
210
  return;
212
211
  }
213
212
  const ids = {
214
- resourceId,
213
+ sessionId,
215
214
  threadId
216
215
  };
217
- const chain = (pendingCaptures.get(resourceId) ?? Promise.resolve()).then(() => captureSessionFilesystem(session, dependencies, ids));
218
- pendingCaptures.set(resourceId, chain);
216
+ const chain = (pendingCaptures.get(sessionId) ?? Promise.resolve()).then(() => captureSessionFilesystem(session, dependencies, ids));
217
+ pendingCaptures.set(sessionId, chain);
219
218
  chain.finally(() => {
220
- if (pendingCaptures.get(resourceId) === chain) pendingCaptures.delete(resourceId);
219
+ if (pendingCaptures.get(sessionId) === chain) pendingCaptures.delete(sessionId);
221
220
  });
222
221
  });
223
222
  return () => {
@@ -1 +1 @@
1
- {"version":3,"file":"filesystem-capture.js","names":[],"sources":["../../src/session/filesystem-capture.ts"],"sourcesContent":["import type { AgentControllerEvent, SessionBeforeAgentEndListener } from '@mastra/core/agent-controller';\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\nimport { peekSessionSandbox, sessionLayout } from '../sandbox/session-sandbox.js';\nimport type { SessionLayoutRepository } from '../sandbox/session-sandbox.js';\n\nimport type { FilesystemFile, FilesystemStorage } from '../storage/domains/filesystem/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { peekSessionEnvironment } from './environment-state-processor.js';\nimport { isMeaningfulToolName } from './first-exec-capture.js';\n\n/** Exit code of `GIT_CHANGED_FILES_SCRIPT` when the checkout is not there: the repository is skipped, not failed. */\nconst CHECKOUT_MISSING_EXIT_CODE = 3;\nconst GIT_CHANGED_FILES_SCRIPT = `\nset -e\nworkdir=$1\nbase_branch=$2\ntest -d \"$workdir\" || exit ${CHECKOUT_MISSING_EXIT_CODE}\nbase=HEAD\nif merge_base=$(git -C \"$workdir\" merge-base HEAD \"origin/$base_branch\" 2>/dev/null); then\n base=$merge_base\nfi\ngit -C \"$workdir\" diff --name-only -z --find-renames --diff-filter=ACMRTUXB \"$base\"\ngit -C \"$workdir\" ls-files --others --exclude-standard -z\n`;\nconst ARTIFACTS_LIST_COMMAND = 'cd \"$1\" && test -d .artifacts && find .artifacts -type f -print0 || true';\n/**\n * Regular files directly under the workspace root written since the session\n * was created ($2 is a UTC `touch -t` stamp, `YYYYMMDDhhmm.SS`), such as the\n * agent's notes. Files the template shipped with predate the session and stay\n * out; the checkouts are covered by git. Dotfiles are never listed: a listed\n * path becomes readable through the thread's file route, and the root is where\n * setup leaves `.env`, `.pgpass`, `.netrc` and other credential files.\n * `touch -t` plus `-newer` works on GNU and BSD find alike; `-newermt\n * \"@epoch\"` is GNU only.\n */\nexport const ROOT_FILES_LIST_COMMAND = [\n 'cd \"$1\" && ref=$(mktemp) && TZ=UTC touch -t \"$2\" \"$ref\"',\n '&& find . -mindepth 1 -maxdepth 1 -type f ! -name \\'.*\\' -newer \"$ref\" -print0',\n '; status=$?; rm -f \"$ref\"; exit $status',\n].join(' ');\n\n/** `touch -t` stamp of a moment, in UTC. */\nexport function touchStamp(date: Date): string {\n const two = (value: number) => String(value).padStart(2, '0');\n return (\n `${date.getUTCFullYear()}${two(date.getUTCMonth() + 1)}${two(date.getUTCDate())}` +\n `${two(date.getUTCHours())}${two(date.getUTCMinutes())}.${two(date.getUTCSeconds())}`\n );\n}\n\nexport interface FilesystemCaptureSession {\n readonly identity: { getResourceId(): string };\n readonly thread: { requireId(): string };\n getWorkspace(): { sandbox?: Pick<WorkspaceSandbox, 'executeCommand'> } | undefined;\n onBeforeAgentEnd(listener: SessionBeforeAgentEndListener): () => void;\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FilesystemCaptureDependencies {\n filesystem: Pick<FilesystemStorage, 'replaceFiles'>;\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;\n };\n}\n\nexport function parseFilesystemCaptureFiles(output: string): FilesystemFile[] {\n const files = new Map<string, FilesystemFile>();\n for (const record of output.split('\\0')) {\n if (!record) continue;\n const path = record.replace(/^\\.\\//, '');\n if (path) files.set(path, { path });\n }\n\n return [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path));\n}\n\nexport async function captureSessionFilesystem(\n session: FilesystemCaptureSession,\n { filesystem, sourceControl }: FilesystemCaptureDependencies,\n ids?: { resourceId: string; threadId: string },\n): Promise<void> {\n try {\n // Scheduled captures pass ids read at schedule time: a session deleted\n // before the queued capture runs has already cleared its thread.\n const resourceId = ids?.resourceId ?? session.identity.getResourceId();\n const threadId = ids?.threadId ?? session.thread.requireId();\n const sourceSession = await sourceControl.sessions.getBySessionId(resourceId);\n // Chat-only sessions run without a workspace; there is nothing to capture.\n const sandbox = session.getWorkspace()?.sandbox;\n if (!sourceSession || !sandbox?.executeCommand) return;\n // The live workdir comes from the per-process memo (the deterministic\n // truth) ONLY — never the persisted observability column, which a row\n // written under a previous provider can point at a workdir that no\n // longer exists (the stale-workdir incident class). No memo entry means\n // no live sandbox worth capturing in this replica; capture is\n // best-effort telemetry, so skip.\n const entry = peekSessionSandbox(sourceSession.id);\n if (!entry) return;\n // Telemetry must never provision a VM: executeCommand lazily starts the\n // sandbox via ensureRunning, so a chat turn that never touched the\n // workspace would otherwise boot (and clone into) a fresh VM just to\n // capture an empty git status. Only capture when the turn already has a\n // running sandbox.\n if (entry.sandbox.status !== 'running') return;\n // Running-but-unresolved should not happen (the start hook resolves the\n // workdir), but capture is best-effort: skip rather than guess.\n const layout = sessionLayout(entry);\n if (!layout) return;\n\n // One git pass per checkout, in parallel so three repositories stay well\n // inside the 10 s readers wait for a pending capture. The session's own\n // repository compares against the session base branch; the others sit on\n // their default branch and compare against that. A repository whose\n // checkout is missing (exit 3) is skipped: it has nothing to list and\n // must not block the others. Any other failure is treated as transient\n // and leaves the previous listing in place.\n const multiRepo = layout.repos.length > 1;\n const environment = multiRepo ? peekSessionEnvironment(sourceSession.sessionId) : undefined;\n const ownRepo = entry.ownRepo.toLowerCase();\n // The boot records the state at its end, after every setup command; until\n // then a secondary checkout compares against its own `origin/HEAD`.\n const baseBranchFor = (repo: SessionLayoutRepository): string => {\n if (repo.slug.toLowerCase() === ownRepo) return sourceSession.baseBranch;\n const state = environment?.repositories.find(\n candidate => candidate.slug.toLowerCase() === repo.slug.toLowerCase(),\n );\n return state?.defaultBranch ?? 'HEAD';\n };\n const targets = layout.repos.map(repo => ({ repo, baseBranch: baseBranchFor(repo) }));\n const executeCommand = sandbox.executeCommand.bind(sandbox);\n const results = await Promise.all(\n targets.map(({ repo, baseBranch }) =>\n executeCommand('sh', ['-c', GIT_CHANGED_FILES_SCRIPT, 'mastracode-changed-files', repo.dir, baseBranch], {\n timeout: 30_000,\n }),\n ),\n );\n const files = new Map<string, FilesystemFile>();\n let captured = 0;\n for (const [index, result] of results.entries()) {\n if (result.exitCode === CHECKOUT_MISSING_EXIT_CODE) continue;\n if (result.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to inspect Git changes.', result.stderr);\n return;\n }\n captured += 1;\n const prefix = targets[index]!.repo.prefix;\n for (const file of parseFilesystemCaptureFiles(result.stdout)) {\n files.set(`${prefix}${file.path}`, { path: `${prefix}${file.path}` });\n }\n }\n if (captured === 0) return;\n\n const artifacts = await executeCommand('sh', ['-c', ARTIFACTS_LIST_COMMAND, 'sh', layout.root], {\n timeout: 30_000,\n });\n if (artifacts.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to list workspace artifacts.', artifacts.stderr);\n return;\n }\n for (const path of artifacts.stdout.split('\\0')) {\n const normalizedPath = path.replace(/^\\.\\//, '');\n if (normalizedPath) {\n files.set(normalizedPath, { path: normalizedPath });\n }\n }\n\n // With several checkouts the root is a plain directory nothing else lists.\n // A failed listing is logged and the git and artifact results still land.\n if (multiRepo) {\n const since = touchStamp(sourceSession.createdAt);\n const rootFiles = await executeCommand('sh', ['-c', ROOT_FILES_LIST_COMMAND, 'sh', layout.root, since], {\n timeout: 30_000,\n });\n if (rootFiles.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to list workspace root files.', rootFiles.stderr);\n } else {\n for (const path of rootFiles.stdout.split('\\0')) {\n const normalizedPath = path.replace(/^\\.\\//, '');\n if (normalizedPath && !normalizedPath.startsWith('.')) {\n files.set(normalizedPath, { path: normalizedPath });\n }\n }\n }\n }\n\n await filesystem.replaceFiles({\n resourceId,\n threadId,\n files: [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path)),\n });\n } catch (error) {\n console.warn('[Factory filesystem capture] Unable to persist files.', error);\n }\n}\n\n/**\n * In-flight capture chains keyed by session resource id. Serves two purposes:\n * readers of the persisted file listing (the /web/workspace routes) await the\n * capture for the turn that just ended instead of racing it, and captures for\n * the same resource stay sequential (last write wins) even when multiple\n * scoped sessions observe the same resource id. In-process only: the factory\n * server that runs the session controller also serves those routes.\n */\nconst pendingCaptures = new Map<string, Promise<void>>();\n\n/**\n * Resolve once the session's in-flight filesystem capture (if any) has\n * persisted, or after `timeoutMs` so a reader that arrives mid-capture is\n * never blocked by the execs' worst-case 30s timeouts. Never rejects: the\n * capture body is fully try/catch contained.\n */\nexport async function waitForPendingFilesystemCapture(resourceId: string, timeoutMs = 10_000): Promise<void> {\n let pending = pendingCaptures.get(resourceId);\n if (!pending) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n const deadline = new Promise<void>(resolve => {\n timer = setTimeout(() => {\n timedOut = true;\n resolve();\n }, timeoutMs);\n });\n try {\n // A turn that ends while we wait chains a successor capture; keep waiting\n // until the latest chain settles so the final snapshot is not lost.\n while (pending && !timedOut) {\n await Promise.race([pending, deadline]);\n const next = pendingCaptures.get(resourceId);\n pending = next === pending ? undefined : next;\n }\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport function observeSessionFilesystem(\n session: FilesystemCaptureSession,\n dependencies: FilesystemCaptureDependencies,\n): () => void {\n let fallbackChain = Promise.resolve();\n // Capture only runs after a turn that actually touched the workspace: a\n // successful workspace tool call both proves the sandbox is awake and is\n // the only way the listing can have changed. Without this gate every chat\n // turn would run two sandbox execs — and since executeCommand resumes an\n // idle VM, a pure-chat turn would keep waking a sandbox it never used.\n // Same bookkeeping as first-exec capture: tool names resolve at tool_end\n // via the tool_start map, and suspended calls survive agent_end so a\n // resumed run's eventual completion still counts for that turn.\n let workspaceTouched = false;\n const toolNames = new Map<string, string>();\n const suspended = new Set<string>();\n const unsubscribeEvents = session.subscribe(event => {\n switch (event.type) {\n case 'tool_start': {\n toolNames.set(event.toolCallId, event.toolName);\n return;\n }\n case 'tool_suspended': {\n suspended.add(event.toolCallId);\n return;\n }\n case 'tool_suspension_cancelled': {\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n return;\n }\n case 'agent_end': {\n for (const id of toolNames.keys()) {\n if (!suspended.has(id)) toolNames.delete(id);\n }\n return;\n }\n case 'tool_end': {\n const toolName = toolNames.get(event.toolCallId);\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n if (event.isError || event.denied) return;\n if (isMeaningfulToolName(toolName)) workspaceTouched = true;\n return;\n }\n }\n });\n const unsubscribeEnd = session.onBeforeAgentEnd(() => {\n // onBeforeAgentEnd fires before agent_end is emitted, so every tool_end\n // of the finishing run has already been observed; read then reset.\n if (!workspaceTouched) return;\n workspaceTouched = false;\n // Chain so captures stay sequential (last write wins), but do NOT return\n // the chain: finishAgentRun awaits every listener before emitting\n // agent_end, and the capture's sandbox execs (git status + artifacts\n // find, 30s timeouts each) must not gate turn completion. Readers that\n // need the fresh listing await the pending capture through\n // waitForPendingFilesystemCapture instead. The un-returned chain cannot\n // leak an unhandled rejection: captureSessionFilesystem's entire body\n // runs inside its own try/catch, and disposal of this listener does not\n // cancel an in-flight chain, so a final turn's capture still persists.\n let resourceId: string | undefined;\n try {\n resourceId = session.identity.getResourceId();\n } catch {\n // No resource id means no route can address this session's listing;\n // keep a per-observer chain so captures still run sequentially.\n fallbackChain = fallbackChain.then(() => captureSessionFilesystem(session, dependencies));\n return;\n }\n // Extend the resource-level chain rather than a per-session one: sessions\n // are deduplicated by (resourceId, scope), so multiple scoped sessions can\n // observe the same resource, and their captures must not interleave.\n let threadId: string;\n try {\n threadId = session.thread.requireId();\n } catch {\n return;\n }\n const ids = { resourceId, threadId };\n const chain = (pendingCaptures.get(resourceId) ?? Promise.resolve()).then(() =>\n captureSessionFilesystem(session, dependencies, ids),\n );\n pendingCaptures.set(resourceId, chain);\n void chain.finally(() => {\n if (pendingCaptures.get(resourceId) === chain) {\n pendingCaptures.delete(resourceId);\n }\n });\n });\n return () => {\n unsubscribeEvents();\n unsubscribeEnd();\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"filesystem-capture.js","names":[],"sources":["../../src/session/filesystem-capture.ts"],"sourcesContent":["import type { AgentControllerEvent, SessionBeforeAgentEndListener } from '@mastra/core/agent-controller';\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\nimport { peekSessionSandbox, sessionLayout } from '../sandbox/session-sandbox.js';\nimport type { SessionLayoutRepository } from '../sandbox/session-sandbox.js';\n\nimport type { FilesystemFile, FilesystemStorage } from '../storage/domains/filesystem/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { peekSessionEnvironment } from './environment-state-processor.js';\nimport { isMeaningfulToolName } from './first-exec-capture.js';\n\n/** Exit code of `GIT_CHANGED_FILES_SCRIPT` when the checkout is not there: the repository is skipped, not failed. */\nconst CHECKOUT_MISSING_EXIT_CODE = 3;\nconst GIT_CHANGED_FILES_SCRIPT = `\nset -e\nworkdir=$1\nbase_branch=$2\ntest -d \"$workdir\" || exit ${CHECKOUT_MISSING_EXIT_CODE}\nbase=HEAD\nif merge_base=$(git -C \"$workdir\" merge-base HEAD \"origin/$base_branch\" 2>/dev/null); then\n base=$merge_base\nfi\ngit -C \"$workdir\" diff --name-only -z --find-renames --diff-filter=ACMRTUXB \"$base\"\ngit -C \"$workdir\" ls-files --others --exclude-standard -z\n`;\nconst ARTIFACTS_LIST_COMMAND = 'cd \"$1\" && test -d .artifacts && find .artifacts -type f -print0 || true';\n/**\n * Regular files directly under the workspace root written since the session\n * was created ($2 is a UTC `touch -t` stamp, `YYYYMMDDhhmm.SS`), such as the\n * agent's notes. Files the template shipped with predate the session and stay\n * out; the checkouts are covered by git. Dotfiles are never listed: a listed\n * path becomes readable through the thread's file route, and the root is where\n * setup leaves `.env`, `.pgpass`, `.netrc` and other credential files.\n * `touch -t` plus `-newer` works on GNU and BSD find alike; `-newermt\n * \"@epoch\"` is GNU only.\n */\nexport const ROOT_FILES_LIST_COMMAND = [\n 'cd \"$1\" && ref=$(mktemp) && TZ=UTC touch -t \"$2\" \"$ref\"',\n '&& find . -mindepth 1 -maxdepth 1 -type f ! -name \\'.*\\' -newer \"$ref\" -print0',\n '; status=$?; rm -f \"$ref\"; exit $status',\n].join(' ');\n\n/** `touch -t` stamp of a moment, in UTC. */\nexport function touchStamp(date: Date): string {\n const two = (value: number) => String(value).padStart(2, '0');\n return (\n `${date.getUTCFullYear()}${two(date.getUTCMonth() + 1)}${two(date.getUTCDate())}` +\n `${two(date.getUTCHours())}${two(date.getUTCMinutes())}.${two(date.getUTCSeconds())}`\n );\n}\n\nexport interface FilesystemCaptureSession {\n readonly identity: { getId(): string };\n readonly thread: { requireId(): string };\n getWorkspace(): { sandbox?: Pick<WorkspaceSandbox, 'executeCommand'> } | undefined;\n onBeforeAgentEnd(listener: SessionBeforeAgentEndListener): () => void;\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FilesystemCaptureDependencies {\n filesystem: Pick<FilesystemStorage, 'replaceFiles'>;\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId'>;\n };\n}\n\nexport function parseFilesystemCaptureFiles(output: string): FilesystemFile[] {\n const files = new Map<string, FilesystemFile>();\n for (const record of output.split('\\0')) {\n if (!record) continue;\n const path = record.replace(/^\\.\\//, '');\n if (path) files.set(path, { path });\n }\n\n return [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path));\n}\n\nexport async function captureSessionFilesystem(\n session: FilesystemCaptureSession,\n { filesystem, sourceControl }: FilesystemCaptureDependencies,\n ids?: { sessionId: string; threadId: string },\n): Promise<void> {\n try {\n // Scheduled captures pass ids read at schedule time: a session deleted\n // before the queued capture runs has already cleared its thread.\n const sessionId = ids?.sessionId ?? session.identity.getId();\n const threadId = ids?.threadId ?? session.thread.requireId();\n const sourceSession = await sourceControl.sessions.getBySessionId(sessionId);\n // Chat-only sessions run without a workspace; there is nothing to capture.\n const sandbox = session.getWorkspace()?.sandbox;\n if (!sourceSession || !sandbox?.executeCommand) return;\n // The live workdir comes from the per-process memo (the deterministic\n // truth) ONLY — never the persisted observability column, which a row\n // written under a previous provider can point at a workdir that no\n // longer exists (the stale-workdir incident class). No memo entry means\n // no live sandbox worth capturing in this replica; capture is\n // best-effort telemetry, so skip.\n const entry = peekSessionSandbox(sourceSession.id);\n if (!entry) return;\n // Telemetry must never provision a VM: executeCommand lazily starts the\n // sandbox via ensureRunning, so a chat turn that never touched the\n // workspace would otherwise boot (and clone into) a fresh VM just to\n // capture an empty git status. Only capture when the turn already has a\n // running sandbox.\n if (entry.sandbox.status !== 'running') return;\n // Running-but-unresolved should not happen (the start hook resolves the\n // workdir), but capture is best-effort: skip rather than guess.\n const layout = sessionLayout(entry);\n if (!layout) return;\n\n // One git pass per checkout, in parallel so three repositories stay well\n // inside the 10 s readers wait for a pending capture. The session's own\n // repository compares against the session base branch; the others sit on\n // their default branch and compare against that. A repository whose\n // checkout is missing (exit 3) is skipped: it has nothing to list and\n // must not block the others. Any other failure is treated as transient\n // and leaves the previous listing in place.\n const multiRepo = layout.repos.length > 1;\n const environment = multiRepo ? peekSessionEnvironment(sourceSession.sessionId) : undefined;\n const ownRepo = entry.ownRepo.toLowerCase();\n // The boot records the state at its end, after every setup command; until\n // then a secondary checkout compares against its own `origin/HEAD`.\n const baseBranchFor = (repo: SessionLayoutRepository): string => {\n if (repo.slug.toLowerCase() === ownRepo) return sourceSession.baseBranch;\n const state = environment?.repositories.find(\n candidate => candidate.slug.toLowerCase() === repo.slug.toLowerCase(),\n );\n return state?.defaultBranch ?? 'HEAD';\n };\n const targets = layout.repos.map(repo => ({ repo, baseBranch: baseBranchFor(repo) }));\n const executeCommand = sandbox.executeCommand.bind(sandbox);\n const results = await Promise.all(\n targets.map(({ repo, baseBranch }) =>\n executeCommand('sh', ['-c', GIT_CHANGED_FILES_SCRIPT, 'mastracode-changed-files', repo.dir, baseBranch], {\n timeout: 30_000,\n }),\n ),\n );\n const files = new Map<string, FilesystemFile>();\n let captured = 0;\n for (const [index, result] of results.entries()) {\n if (result.exitCode === CHECKOUT_MISSING_EXIT_CODE) continue;\n if (result.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to inspect Git changes.', result.stderr);\n return;\n }\n captured += 1;\n const prefix = targets[index]!.repo.prefix;\n for (const file of parseFilesystemCaptureFiles(result.stdout)) {\n files.set(`${prefix}${file.path}`, { path: `${prefix}${file.path}` });\n }\n }\n if (captured === 0) return;\n\n const artifacts = await executeCommand('sh', ['-c', ARTIFACTS_LIST_COMMAND, 'sh', layout.root], {\n timeout: 30_000,\n });\n if (artifacts.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to list workspace artifacts.', artifacts.stderr);\n return;\n }\n for (const path of artifacts.stdout.split('\\0')) {\n const normalizedPath = path.replace(/^\\.\\//, '');\n if (normalizedPath) {\n files.set(normalizedPath, { path: normalizedPath });\n }\n }\n\n // With several checkouts the root is a plain directory nothing else lists.\n // A failed listing is logged and the git and artifact results still land.\n if (multiRepo) {\n const since = touchStamp(sourceSession.createdAt);\n const rootFiles = await executeCommand('sh', ['-c', ROOT_FILES_LIST_COMMAND, 'sh', layout.root, since], {\n timeout: 30_000,\n });\n if (rootFiles.exitCode !== 0) {\n console.warn('[Factory filesystem capture] Unable to list workspace root files.', rootFiles.stderr);\n } else {\n for (const path of rootFiles.stdout.split('\\0')) {\n const normalizedPath = path.replace(/^\\.\\//, '');\n if (normalizedPath && !normalizedPath.startsWith('.')) {\n files.set(normalizedPath, { path: normalizedPath });\n }\n }\n }\n }\n\n // The listing describes the session's workspace, not its memory resource,\n // which the session shares with others: rows are keyed by the session id.\n await filesystem.replaceFiles({\n resourceId: sessionId,\n threadId,\n files: [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path)),\n });\n } catch (error) {\n console.warn('[Factory filesystem capture] Unable to persist files.', error);\n }\n}\n\n/**\n * In-flight capture chains keyed by session id. Serves two purposes: readers\n * of the persisted file listing (the /web/workspace routes) await the capture\n * for the turn that just ended instead of racing it, and captures for the same\n * session stay sequential (last write wins). In-process only: the factory\n * server that runs the session controller also serves those routes.\n */\nconst pendingCaptures = new Map<string, Promise<void>>();\n\n/**\n * Resolve once the session's in-flight filesystem capture (if any) has\n * persisted, or after `timeoutMs` so a reader that arrives mid-capture is\n * never blocked by the execs' worst-case 30s timeouts. Never rejects: the\n * capture body is fully try/catch contained.\n */\nexport async function waitForPendingFilesystemCapture(sessionId: string, timeoutMs = 10_000): Promise<void> {\n let pending = pendingCaptures.get(sessionId);\n if (!pending) return;\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n const deadline = new Promise<void>(resolve => {\n timer = setTimeout(() => {\n timedOut = true;\n resolve();\n }, timeoutMs);\n });\n try {\n // A turn that ends while we wait chains a successor capture; keep waiting\n // until the latest chain settles so the final snapshot is not lost.\n while (pending && !timedOut) {\n await Promise.race([pending, deadline]);\n const next = pendingCaptures.get(sessionId);\n pending = next === pending ? undefined : next;\n }\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport function observeSessionFilesystem(\n session: FilesystemCaptureSession,\n dependencies: FilesystemCaptureDependencies,\n): () => void {\n let fallbackChain = Promise.resolve();\n // Capture only runs after a turn that actually touched the workspace: a\n // successful workspace tool call both proves the sandbox is awake and is\n // the only way the listing can have changed. Without this gate every chat\n // turn would run two sandbox execs — and since executeCommand resumes an\n // idle VM, a pure-chat turn would keep waking a sandbox it never used.\n // Same bookkeeping as first-exec capture: tool names resolve at tool_end\n // via the tool_start map, and suspended calls survive agent_end so a\n // resumed run's eventual completion still counts for that turn.\n let workspaceTouched = false;\n const toolNames = new Map<string, string>();\n const suspended = new Set<string>();\n const unsubscribeEvents = session.subscribe(event => {\n switch (event.type) {\n case 'tool_start': {\n toolNames.set(event.toolCallId, event.toolName);\n return;\n }\n case 'tool_suspended': {\n suspended.add(event.toolCallId);\n return;\n }\n case 'tool_suspension_cancelled': {\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n return;\n }\n case 'agent_end': {\n for (const id of toolNames.keys()) {\n if (!suspended.has(id)) toolNames.delete(id);\n }\n return;\n }\n case 'tool_end': {\n const toolName = toolNames.get(event.toolCallId);\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n if (event.isError || event.denied) return;\n if (isMeaningfulToolName(toolName)) workspaceTouched = true;\n return;\n }\n }\n });\n const unsubscribeEnd = session.onBeforeAgentEnd(() => {\n // onBeforeAgentEnd fires before agent_end is emitted, so every tool_end\n // of the finishing run has already been observed; read then reset.\n if (!workspaceTouched) return;\n workspaceTouched = false;\n // Chain so captures stay sequential (last write wins), but do NOT return\n // the chain: finishAgentRun awaits every listener before emitting\n // agent_end, and the capture's sandbox execs (git status + artifacts\n // find, 30s timeouts each) must not gate turn completion. Readers that\n // need the fresh listing await the pending capture through\n // waitForPendingFilesystemCapture instead. The un-returned chain cannot\n // leak an unhandled rejection: captureSessionFilesystem's entire body\n // runs inside its own try/catch, and disposal of this listener does not\n // cancel an in-flight chain, so a final turn's capture still persists.\n let sessionId: string;\n try {\n sessionId = session.identity.getId();\n } catch {\n // No identity means no route can address this session's listing; keep\n // a per-observer chain so captures still run sequentially.\n fallbackChain = fallbackChain.then(() => captureSessionFilesystem(session, dependencies));\n return;\n }\n let threadId: string;\n try {\n threadId = session.thread.requireId();\n } catch {\n return;\n }\n const ids = { sessionId, threadId };\n const chain = (pendingCaptures.get(sessionId) ?? Promise.resolve()).then(() =>\n captureSessionFilesystem(session, dependencies, ids),\n );\n pendingCaptures.set(sessionId, chain);\n void chain.finally(() => {\n if (pendingCaptures.get(sessionId) === chain) {\n pendingCaptures.delete(sessionId);\n }\n });\n });\n return () => {\n unsubscribeEvents();\n unsubscribeEnd();\n };\n}\n"],"mappings":"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"}
@@ -2,7 +2,7 @@ import type { AgentControllerEvent } from '@mastra/core/agent-controller';
2
2
  import type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';
3
3
  export interface FirstExecCaptureSession {
4
4
  readonly identity: {
5
- getResourceId(): string;
5
+ getId(): string;
6
6
  };
7
7
  subscribe(listener: (event: AgentControllerEvent) => void): () => void;
8
8
  }
@@ -1 +1 @@
1
- {"version":3,"file":"first-exec-capture.d.ts","sourceRoot":"","sources":["../../src/session/first-exec-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAG1E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,4BAA4B;IAC3C,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,yBAAyB,CAAC,CAAC;KACnF,CAAC;CACH;AA6BD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAItE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,uBAAuB,EAChC,EAAE,aAAa,EAAE,EAAE,4BAA4B,GAC9C,MAAM,IAAI,CAoDZ"}
1
+ {"version":3,"file":"first-exec-capture.d.ts","sourceRoot":"","sources":["../../src/session/first-exec-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAG1E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE;QAAE,KAAK,IAAI,MAAM,CAAA;KAAE,CAAC;IACvC,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,4BAA4B;IAC3C,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,yBAAyB,CAAC,CAAC;KACnF,CAAC;CACH;AA6BD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAItE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,uBAAuB,EAChC,EAAE,aAAa,EAAE,EAAE,4BAA4B,GAC9C,MAAM,IAAI,CAoDZ"}
@@ -78,7 +78,7 @@ function observeSessionFirstExec(session, { sourceControl }) {
78
78
  if (!isMeaningfulToolName(toolName)) return;
79
79
  seen = true;
80
80
  unsubscribe();
81
- sourceControl.sessions.markFirstMeaningfulExec({ sessionId: session.identity.getResourceId() }).catch((error) => console.warn("[Factory first-exec capture] Unable to persist first exec time.", error));
81
+ sourceControl.sessions.markFirstMeaningfulExec({ sessionId: session.identity.getId() }).catch((error) => console.warn("[Factory first-exec capture] Unable to persist first exec time.", error));
82
82
  return;
83
83
  }
84
84
  }
@@ -1 +1 @@
1
- {"version":3,"file":"first-exec-capture.js","names":[],"sources":["../../src/session/first-exec-capture.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\nimport { WORKSPACE_TOOLS_PREFIX } from '@mastra/core/workspace';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\nexport interface FirstExecCaptureSession {\n readonly identity: { getResourceId(): string };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FirstExecCaptureDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'markFirstMeaningfulExec'>;\n };\n}\n\n/**\n * Tool names that count as \"meaningful exec\" independent of the workspace\n * prefix. The workspace tools get remapped from `mastra_workspace_*` to\n * mastracode-friendly names (see `mastracode/sdk/src/tool-names.ts`), and\n * this listener runs on the emitted tool name after remap, so both spellings\n * need to match.\n */\nconst MEANINGFUL_TOOL_NAMES: ReadonlySet<string> = new Set([\n // Filesystem (post-remap)\n 'view',\n 'write_file',\n 'string_replace_lsp',\n 'find_files',\n 'delete_file',\n 'file_stat',\n 'mkdir',\n // Search\n 'search_content',\n // Code intelligence\n 'ast_smart_edit',\n 'lsp_inspect',\n // Sandbox\n 'execute_command',\n 'get_process_output',\n 'kill_process',\n]);\n\nexport function isMeaningfulToolName(name: string | undefined): boolean {\n if (!name) return false;\n if (name.startsWith(`${WORKSPACE_TOOLS_PREFIX}_`)) return true;\n return MEANINGFUL_TOOL_NAMES.has(name);\n}\n\n/**\n * Record when a session's agent completed its first successful meaningful\n * tool call (the TTFME anchor — \"time to first meaningful exec\").\n *\n * Meaningful = a workspace tool the agent invoked itself (filesystem,\n * search, code intelligence, sandbox exec, process control). We accept any\n * tool whose name starts with the workspace prefix `mastra_workspace_` or\n * matches the post-remap mastracode tool names. Non-workspace tools (memory,\n * notification inbox, subagent, etc.) and skill-loader / preflight /\n * materializer `sandbox.executeCommand()` calls that never flow through the\n * tool layer are intentionally excluded.\n *\n * Failed tool calls (`isError === true`) don't count. Approval-denied and\n * abort-while-parked tool completions (`denied === true`) also don't count:\n * the run-engine emits `tool_end` with `isError: false` for those paths\n * because the tool didn't fail, but it also never ran, so treating them as\n * \"meaningful exec\" would re-introduce the same contamination this fix is\n * removing on the message side. The listener stays subscribed until a\n * successful qualifying end, then unsubscribes. The storage write is guarded\n * (`first_meaningful_exec_at IS NULL`), so restarts, re-materialized\n * sessions, and sessions without a source-control row are no-ops.\n */\nexport function observeSessionFirstExec(\n session: FirstExecCaptureSession,\n { sourceControl }: FirstExecCaptureDependencies,\n): () => void {\n let seen = false;\n // toolCallId -> toolName. Populated on `tool_start`, consumed on `tool_end`.\n // Suspended entries survive `agent_end` so the resumed run can still resolve\n // its tool name when the eventual `tool_end` arrives.\n const toolNames = new Map<string, string>();\n const suspended = new Set<string>();\n const unsubscribe = session.subscribe(event => {\n if (seen) return;\n switch (event.type) {\n case 'tool_start': {\n toolNames.set(event.toolCallId, event.toolName);\n return;\n }\n case 'tool_suspended': {\n suspended.add(event.toolCallId);\n return;\n }\n case 'tool_suspension_cancelled': {\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n return;\n }\n case 'agent_end': {\n // Drop any in-flight starts that will never see a matching `tool_end`\n // on this run. Genuinely suspended calls stay so their eventual\n // resume-time `tool_end` can still map back to a tool name. Without\n // this, sessions that repeatedly abort or error before a qualifying\n // completion would accumulate stale entries for the entire lifetime\n // of the subscription.\n for (const id of toolNames.keys()) {\n if (!suspended.has(id)) toolNames.delete(id);\n }\n return;\n }\n case 'tool_end': {\n const toolName = toolNames.get(event.toolCallId);\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n if (event.isError) return;\n if (event.denied) return;\n if (!isMeaningfulToolName(toolName)) return;\n seen = true;\n unsubscribe();\n void sourceControl.sessions\n .markFirstMeaningfulExec({ sessionId: session.identity.getResourceId() })\n .catch(error => console.warn('[Factory first-exec capture] Unable to persist first exec time.', error));\n return;\n }\n }\n });\n return unsubscribe;\n}\n"],"mappings":";;;;;;;;;AAuBA,MAAM,wCAA6C,IAAI,IAAI;CAEzD;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CAEA;CACA;CAEA;CACA;CACA;AACF,CAAC;AAED,SAAgB,qBAAqB,MAAmC;CACtE,IAAI,CAAC,MAAM,OAAO;CAClB,IAAI,KAAK,WAAW,GAAG,uBAAuB,EAAE,GAAG,OAAO;CAC1D,OAAO,sBAAsB,IAAI,IAAI;AACvC;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,wBACd,SACA,EAAE,iBACU;CACZ,IAAI,OAAO;CAIX,MAAM,4BAAY,IAAI,IAAoB;CAC1C,MAAM,4BAAY,IAAI,IAAY;CAClC,MAAM,cAAc,QAAQ,WAAU,UAAS;EAC7C,IAAI,MAAM;EACV,QAAQ,MAAM,MAAd;GACE,KAAK;IACH,UAAU,IAAI,MAAM,YAAY,MAAM,QAAQ;IAC9C;GAEF,KAAK;IACH,UAAU,IAAI,MAAM,UAAU;IAC9B;GAEF,KAAK;IACH,UAAU,OAAO,MAAM,UAAU;IACjC,UAAU,OAAO,MAAM,UAAU;IACjC;GAEF,KAAK;IAOH,KAAK,MAAM,MAAM,UAAU,KAAK,GAC9B,IAAI,CAAC,UAAU,IAAI,EAAE,GAAG,UAAU,OAAO,EAAE;IAE7C;GAEF,KAAK,YAAY;IACf,MAAM,WAAW,UAAU,IAAI,MAAM,UAAU;IAC/C,UAAU,OAAO,MAAM,UAAU;IACjC,UAAU,OAAO,MAAM,UAAU;IACjC,IAAI,MAAM,SAAS;IACnB,IAAI,MAAM,QAAQ;IAClB,IAAI,CAAC,qBAAqB,QAAQ,GAAG;IACrC,OAAO;IACP,YAAY;IACZ,cAAmB,SAChB,wBAAwB,EAAE,WAAW,QAAQ,SAAS,cAAc,EAAE,CAAC,CAAC,CACxE,OAAM,UAAS,QAAQ,KAAK,mEAAmE,KAAK,CAAC;IACxG;GACF;EACF;CACF,CAAC;CACD,OAAO;AACT"}
1
+ {"version":3,"file":"first-exec-capture.js","names":[],"sources":["../../src/session/first-exec-capture.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\nimport { WORKSPACE_TOOLS_PREFIX } from '@mastra/core/workspace';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\nexport interface FirstExecCaptureSession {\n readonly identity: { getId(): string };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FirstExecCaptureDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'markFirstMeaningfulExec'>;\n };\n}\n\n/**\n * Tool names that count as \"meaningful exec\" independent of the workspace\n * prefix. The workspace tools get remapped from `mastra_workspace_*` to\n * mastracode-friendly names (see `mastracode/sdk/src/tool-names.ts`), and\n * this listener runs on the emitted tool name after remap, so both spellings\n * need to match.\n */\nconst MEANINGFUL_TOOL_NAMES: ReadonlySet<string> = new Set([\n // Filesystem (post-remap)\n 'view',\n 'write_file',\n 'string_replace_lsp',\n 'find_files',\n 'delete_file',\n 'file_stat',\n 'mkdir',\n // Search\n 'search_content',\n // Code intelligence\n 'ast_smart_edit',\n 'lsp_inspect',\n // Sandbox\n 'execute_command',\n 'get_process_output',\n 'kill_process',\n]);\n\nexport function isMeaningfulToolName(name: string | undefined): boolean {\n if (!name) return false;\n if (name.startsWith(`${WORKSPACE_TOOLS_PREFIX}_`)) return true;\n return MEANINGFUL_TOOL_NAMES.has(name);\n}\n\n/**\n * Record when a session's agent completed its first successful meaningful\n * tool call (the TTFME anchor — \"time to first meaningful exec\").\n *\n * Meaningful = a workspace tool the agent invoked itself (filesystem,\n * search, code intelligence, sandbox exec, process control). We accept any\n * tool whose name starts with the workspace prefix `mastra_workspace_` or\n * matches the post-remap mastracode tool names. Non-workspace tools (memory,\n * notification inbox, subagent, etc.) and skill-loader / preflight /\n * materializer `sandbox.executeCommand()` calls that never flow through the\n * tool layer are intentionally excluded.\n *\n * Failed tool calls (`isError === true`) don't count. Approval-denied and\n * abort-while-parked tool completions (`denied === true`) also don't count:\n * the run-engine emits `tool_end` with `isError: false` for those paths\n * because the tool didn't fail, but it also never ran, so treating them as\n * \"meaningful exec\" would re-introduce the same contamination this fix is\n * removing on the message side. The listener stays subscribed until a\n * successful qualifying end, then unsubscribes. The storage write is guarded\n * (`first_meaningful_exec_at IS NULL`), so restarts, re-materialized\n * sessions, and sessions without a source-control row are no-ops.\n */\nexport function observeSessionFirstExec(\n session: FirstExecCaptureSession,\n { sourceControl }: FirstExecCaptureDependencies,\n): () => void {\n let seen = false;\n // toolCallId -> toolName. Populated on `tool_start`, consumed on `tool_end`.\n // Suspended entries survive `agent_end` so the resumed run can still resolve\n // its tool name when the eventual `tool_end` arrives.\n const toolNames = new Map<string, string>();\n const suspended = new Set<string>();\n const unsubscribe = session.subscribe(event => {\n if (seen) return;\n switch (event.type) {\n case 'tool_start': {\n toolNames.set(event.toolCallId, event.toolName);\n return;\n }\n case 'tool_suspended': {\n suspended.add(event.toolCallId);\n return;\n }\n case 'tool_suspension_cancelled': {\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n return;\n }\n case 'agent_end': {\n // Drop any in-flight starts that will never see a matching `tool_end`\n // on this run. Genuinely suspended calls stay so their eventual\n // resume-time `tool_end` can still map back to a tool name. Without\n // this, sessions that repeatedly abort or error before a qualifying\n // completion would accumulate stale entries for the entire lifetime\n // of the subscription.\n for (const id of toolNames.keys()) {\n if (!suspended.has(id)) toolNames.delete(id);\n }\n return;\n }\n case 'tool_end': {\n const toolName = toolNames.get(event.toolCallId);\n toolNames.delete(event.toolCallId);\n suspended.delete(event.toolCallId);\n if (event.isError) return;\n if (event.denied) return;\n if (!isMeaningfulToolName(toolName)) return;\n seen = true;\n unsubscribe();\n void sourceControl.sessions\n .markFirstMeaningfulExec({ sessionId: session.identity.getId() })\n .catch(error => console.warn('[Factory first-exec capture] Unable to persist first exec time.', error));\n return;\n }\n }\n });\n return unsubscribe;\n}\n"],"mappings":";;;;;;;;;AAuBA,MAAM,wCAA6C,IAAI,IAAI;CAEzD;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CAEA;CACA;CAEA;CACA;CACA;AACF,CAAC;AAED,SAAgB,qBAAqB,MAAmC;CACtE,IAAI,CAAC,MAAM,OAAO;CAClB,IAAI,KAAK,WAAW,GAAG,uBAAuB,EAAE,GAAG,OAAO;CAC1D,OAAO,sBAAsB,IAAI,IAAI;AACvC;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,wBACd,SACA,EAAE,iBACU;CACZ,IAAI,OAAO;CAIX,MAAM,4BAAY,IAAI,IAAoB;CAC1C,MAAM,4BAAY,IAAI,IAAY;CAClC,MAAM,cAAc,QAAQ,WAAU,UAAS;EAC7C,IAAI,MAAM;EACV,QAAQ,MAAM,MAAd;GACE,KAAK;IACH,UAAU,IAAI,MAAM,YAAY,MAAM,QAAQ;IAC9C;GAEF,KAAK;IACH,UAAU,IAAI,MAAM,UAAU;IAC9B;GAEF,KAAK;IACH,UAAU,OAAO,MAAM,UAAU;IACjC,UAAU,OAAO,MAAM,UAAU;IACjC;GAEF,KAAK;IAOH,KAAK,MAAM,MAAM,UAAU,KAAK,GAC9B,IAAI,CAAC,UAAU,IAAI,EAAE,GAAG,UAAU,OAAO,EAAE;IAE7C;GAEF,KAAK,YAAY;IACf,MAAM,WAAW,UAAU,IAAI,MAAM,UAAU;IAC/C,UAAU,OAAO,MAAM,UAAU;IACjC,UAAU,OAAO,MAAM,UAAU;IACjC,IAAI,MAAM,SAAS;IACnB,IAAI,MAAM,QAAQ;IAClB,IAAI,CAAC,qBAAqB,QAAQ,GAAG;IACrC,OAAO;IACP,YAAY;IACZ,cAAmB,SAChB,wBAAwB,EAAE,WAAW,QAAQ,SAAS,MAAM,EAAE,CAAC,CAAC,CAChE,OAAM,UAAS,QAAQ,KAAK,mEAAmE,KAAK,CAAC;IACxG;GACF;EACF;CACF,CAAC;CACD,OAAO;AACT"}
@@ -2,7 +2,7 @@ import type { AgentControllerEvent } from '@mastra/core/agent-controller';
2
2
  import type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';
3
3
  export interface FirstMessageCaptureSession {
4
4
  readonly identity: {
5
- getResourceId(): string;
5
+ getId(): string;
6
6
  };
7
7
  subscribe(listener: (event: AgentControllerEvent) => void): () => void;
8
8
  }
@@ -1 +1 @@
1
- {"version":3,"file":"first-message-capture.d.ts","sourceRoot":"","sources":["../../src/session/first-message-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAE1E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,QAAQ,EAAE;QAAE,aAAa,IAAI,MAAM,CAAA;KAAE,CAAC;IAC/C,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,+BAA+B;IAC9C,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC,CAAC;KAC5E,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,0BAA0B,EACnC,EAAE,aAAa,EAAE,EAAE,+BAA+B,GACjD,MAAM,IAAI,CAaZ"}
1
+ {"version":3,"file":"first-message-capture.d.ts","sourceRoot":"","sources":["../../src/session/first-message-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAE1E,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,QAAQ,EAAE;QAAE,KAAK,IAAI,MAAM,CAAA;KAAE,CAAC;IACvC,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxE;AAED,MAAM,WAAW,+BAA+B;IAC9C,aAAa,EAAE;QACb,QAAQ,EAAE,IAAI,CAAC,0BAA0B,CAAC,UAAU,CAAC,EAAE,kBAAkB,CAAC,CAAC;KAC5E,CAAC;CACH;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,0BAA0B,EACnC,EAAE,aAAa,EAAE,EAAE,+BAA+B,GACjD,MAAM,IAAI,CAaZ"}
@@ -25,7 +25,7 @@ function observeSessionFirstMessage(session, { sourceControl }) {
25
25
  if (role !== "user" && role !== "assistant") return;
26
26
  seen = true;
27
27
  unsubscribe();
28
- sourceControl.sessions.markFirstMessage({ sessionId: session.identity.getResourceId() }).catch((error) => console.warn("[Factory first-message capture] Unable to persist first message time.", error));
28
+ sourceControl.sessions.markFirstMessage({ sessionId: session.identity.getId() }).catch((error) => console.warn("[Factory first-message capture] Unable to persist first message time.", error));
29
29
  });
30
30
  return unsubscribe;
31
31
  }
@@ -1 +1 @@
1
- {"version":3,"file":"first-message-capture.js","names":[],"sources":["../../src/session/first-message-capture.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\nexport interface FirstMessageCaptureSession {\n readonly identity: { getResourceId(): string };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FirstMessageCaptureDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'markFirstMessage'>;\n };\n}\n\n/**\n * Record when a session's agent first produced an actual conversational\n * message (the TTFI anchor — \"time to first interaction\").\n *\n * We stamp on the first `message_start` whose role is `user` or `assistant`.\n * The coordinator writes `role='signal'` rows into the thread (skill loads,\n * phase markers, memory reminders) before the model is ever invoked; those\n * signals used to trigger a stamp via `agent_start`, giving every session —\n * including zero-message model-init failures — a false TTFI of a few seconds.\n * Gating on real conversational roles keeps the TTFI column honest: sessions\n * that never produced a user or assistant message stay NULL and drop out of\n * TTFI percentiles instead of contaminating them.\n *\n * The listener unsubscribes after the first qualifying message; the storage\n * write is guarded (`first_message_at IS NULL`), so restarts, re-materialized\n * sessions, and sessions without a source-control row (chat-only channels)\n * are no-ops.\n */\nexport function observeSessionFirstMessage(\n session: FirstMessageCaptureSession,\n { sourceControl }: FirstMessageCaptureDependencies,\n): () => void {\n let seen = false;\n const unsubscribe = session.subscribe(event => {\n if (seen || event.type !== 'message_start') return;\n const role = event.message.role;\n if (role !== 'user' && role !== 'assistant') return;\n seen = true;\n unsubscribe();\n void sourceControl.sessions\n .markFirstMessage({ sessionId: session.identity.getResourceId() })\n .catch(error => console.warn('[Factory first-message capture] Unable to persist first message time.', error));\n });\n return unsubscribe;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,2BACd,SACA,EAAE,iBACU;CACZ,IAAI,OAAO;CACX,MAAM,cAAc,QAAQ,WAAU,UAAS;EAC7C,IAAI,QAAQ,MAAM,SAAS,iBAAiB;EAC5C,MAAM,OAAO,MAAM,QAAQ;EAC3B,IAAI,SAAS,UAAU,SAAS,aAAa;EAC7C,OAAO;EACP,YAAY;EACZ,cAAmB,SAChB,iBAAiB,EAAE,WAAW,QAAQ,SAAS,cAAc,EAAE,CAAC,CAAC,CACjE,OAAM,UAAS,QAAQ,KAAK,yEAAyE,KAAK,CAAC;CAChH,CAAC;CACD,OAAO;AACT"}
1
+ {"version":3,"file":"first-message-capture.js","names":[],"sources":["../../src/session/first-message-capture.ts"],"sourcesContent":["import type { AgentControllerEvent } from '@mastra/core/agent-controller';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\n\nexport interface FirstMessageCaptureSession {\n readonly identity: { getId(): string };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface FirstMessageCaptureDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'markFirstMessage'>;\n };\n}\n\n/**\n * Record when a session's agent first produced an actual conversational\n * message (the TTFI anchor — \"time to first interaction\").\n *\n * We stamp on the first `message_start` whose role is `user` or `assistant`.\n * The coordinator writes `role='signal'` rows into the thread (skill loads,\n * phase markers, memory reminders) before the model is ever invoked; those\n * signals used to trigger a stamp via `agent_start`, giving every session —\n * including zero-message model-init failures — a false TTFI of a few seconds.\n * Gating on real conversational roles keeps the TTFI column honest: sessions\n * that never produced a user or assistant message stay NULL and drop out of\n * TTFI percentiles instead of contaminating them.\n *\n * The listener unsubscribes after the first qualifying message; the storage\n * write is guarded (`first_message_at IS NULL`), so restarts, re-materialized\n * sessions, and sessions without a source-control row (chat-only channels)\n * are no-ops.\n */\nexport function observeSessionFirstMessage(\n session: FirstMessageCaptureSession,\n { sourceControl }: FirstMessageCaptureDependencies,\n): () => void {\n let seen = false;\n const unsubscribe = session.subscribe(event => {\n if (seen || event.type !== 'message_start') return;\n const role = event.message.role;\n if (role !== 'user' && role !== 'assistant') return;\n seen = true;\n unsubscribe();\n void sourceControl.sessions\n .markFirstMessage({ sessionId: session.identity.getId() })\n .catch(error => console.warn('[Factory first-message capture] Unable to persist first message time.', error));\n });\n return unsubscribe;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,2BACd,SACA,EAAE,iBACU;CACZ,IAAI,OAAO;CACX,MAAM,cAAc,QAAQ,WAAU,UAAS;EAC7C,IAAI,QAAQ,MAAM,SAAS,iBAAiB;EAC5C,MAAM,OAAO,MAAM,QAAQ;EAC3B,IAAI,SAAS,UAAU,SAAS,aAAa;EAC7C,OAAO;EACP,YAAY;EACZ,cAAmB,SAChB,iBAAiB,EAAE,WAAW,QAAQ,SAAS,MAAM,EAAE,CAAC,CAAC,CACzD,OAAM,UAAS,QAAQ,KAAK,yEAAyE,KAAK,CAAC;CAChH,CAAC;CACD,OAAO;AACT"}
@@ -35,7 +35,7 @@ export interface ParkedRun {
35
35
  * The sessions this process has materialized, by session id.
36
36
  *
37
37
  * Membership comes from the controller's notifications rather than
38
- * `getSessionByResource`, which hands back the pending creation — awaiting it
38
+ * `getSession`, which hands back the pending creation — awaiting it
39
39
  * from a request blocks for as long as the session takes to materialize its
40
40
  * sandbox, minutes on a cold clone. Notifications only fire once a session
41
41
  * exists, so a read never waits.
@@ -3,7 +3,7 @@
3
3
  * The sessions this process has materialized, by session id.
4
4
  *
5
5
  * Membership comes from the controller's notifications rather than
6
- * `getSessionByResource`, which hands back the pending creation — awaiting it
6
+ * `getSession`, which hands back the pending creation — awaiting it
7
7
  * from a request blocks for as long as the session takes to materialize its
8
8
  * sandbox, minutes on a cold clone. Notifications only fire once a session
9
9
  * exists, so a read never waits.