@mastra/factory 0.25.0-alpha.4 → 0.25.0-alpha.5

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 (301) hide show
  1. package/dist/auth.js +1 -2
  2. package/dist/boards/define-board.js +1 -2
  3. package/dist/boards/index.js +1 -2
  4. package/dist/boards/registry.js +1 -2
  5. package/dist/boards/relocate.js +1 -2
  6. package/dist/boards/review.js +1 -2
  7. package/dist/boards/semantics.js +1 -2
  8. package/dist/boards/transition-policy.js +1 -2
  9. package/dist/boards/work-tool-rules.js +1 -2
  10. package/dist/boards/work-transition-policy.js +1 -2
  11. package/dist/boards/work.js +1 -2
  12. package/dist/environment/build-runner.js +1 -2
  13. package/dist/environment/build-schedule.js +1 -2
  14. package/dist/environment/build-workflow.js +1 -2
  15. package/dist/environment/build.js +1 -2
  16. package/dist/environment/heads.js +1 -2
  17. package/dist/factory.d.ts +4 -2
  18. package/dist/factory.d.ts.map +1 -1
  19. package/dist/factory.js +43 -16
  20. package/dist/factory.js.map +1 -1
  21. package/dist/feed-events.js +1 -2
  22. package/dist/index.js +2 -2
  23. package/dist/integrations/channel-thread-announcements.d.ts +8 -0
  24. package/dist/integrations/channel-thread-announcements.d.ts.map +1 -0
  25. package/dist/integrations/channel-thread-announcements.js +16 -0
  26. package/dist/integrations/channel-thread-announcements.js.map +1 -0
  27. package/dist/integrations/github/acceptance-labels.js +1 -2
  28. package/dist/integrations/github/app-identity.js +1 -2
  29. package/dist/integrations/github/commits.js +1 -2
  30. package/dist/integrations/github/config.js +1 -2
  31. package/dist/integrations/github/default-rules.js +2 -3
  32. package/dist/integrations/github/integration.js +3 -4
  33. package/dist/integrations/github/issue-reconciler.js +1 -2
  34. package/dist/integrations/github/pat.js +1 -2
  35. package/dist/integrations/github/provenance.d.ts.map +1 -1
  36. package/dist/integrations/github/provenance.js +2 -5
  37. package/dist/integrations/github/provenance.js.map +1 -1
  38. package/dist/integrations/github/reconcile-worker.js +1 -2
  39. package/dist/integrations/github/reconciliation-config.js +1 -2
  40. package/dist/integrations/github/routes.js +4 -5
  41. package/dist/integrations/github/rules.d.ts.map +1 -1
  42. package/dist/integrations/github/rules.js +14 -3
  43. package/dist/integrations/github/rules.js.map +1 -1
  44. package/dist/integrations/github/sandbox-release.js +1 -2
  45. package/dist/integrations/github/sandbox.js +1 -2
  46. package/dist/integrations/github/session-subscriptions.js +1 -2
  47. package/dist/integrations/github/settle-or-abort.js +1 -2
  48. package/dist/integrations/github/stale-reviews.js +1 -2
  49. package/dist/integrations/github/subscriptions.js +1 -2
  50. package/dist/integrations/github/token-refresh.js +1 -2
  51. package/dist/integrations/github/webhook.d.ts.map +1 -1
  52. package/dist/integrations/github/webhook.js +17 -3
  53. package/dist/integrations/github/webhook.js.map +1 -1
  54. package/dist/integrations/gitlab/agent-tools.js +1 -2
  55. package/dist/integrations/gitlab/api.js +1 -2
  56. package/dist/integrations/gitlab/default-rules.js +1 -2
  57. package/dist/integrations/gitlab/integration.js +2 -3
  58. package/dist/integrations/gitlab/issue-reconciler.js +1 -2
  59. package/dist/integrations/gitlab/merge-request-reconciler.js +1 -2
  60. package/dist/integrations/gitlab/reconciler.js +1 -2
  61. package/dist/integrations/gitlab/reconciliation-config.js +1 -2
  62. package/dist/integrations/gitlab/routes.js +1 -2
  63. package/dist/integrations/gitlab/rules.js +1 -2
  64. package/dist/integrations/gitlab/session-subscriptions.js +1 -2
  65. package/dist/integrations/gitlab/subscriptions.js +1 -2
  66. package/dist/integrations/gitlab/version-control.js +1 -2
  67. package/dist/integrations/gitlab/webhook-dispatch.js +1 -2
  68. package/dist/integrations/gitlab/webhook.js +1 -2
  69. package/dist/integrations/incidentio/agent-tools.js +1 -2
  70. package/dist/integrations/incidentio/api.js +1 -2
  71. package/dist/integrations/incidentio/claim.js +1 -2
  72. package/dist/integrations/incidentio/default-rules.js +1 -2
  73. package/dist/integrations/incidentio/intake.js +1 -2
  74. package/dist/integrations/incidentio/integration.js +1 -2
  75. package/dist/integrations/incidentio/issue-reconciler.js +1 -2
  76. package/dist/integrations/incidentio/reconciliation-config.js +1 -2
  77. package/dist/integrations/incidentio/routes.js +1 -2
  78. package/dist/integrations/incidentio/rules.js +1 -2
  79. package/dist/integrations/issue-reconcile-worker.js +1 -2
  80. package/dist/integrations/issue-reconciler.js +1 -2
  81. package/dist/integrations/jira/adf.js +1 -2
  82. package/dist/integrations/jira/agent-tools.js +1 -2
  83. package/dist/integrations/jira/api.js +1 -2
  84. package/dist/integrations/jira/claim.js +1 -2
  85. package/dist/integrations/jira/default-rules.js +1 -2
  86. package/dist/integrations/jira/integration.js +1 -2
  87. package/dist/integrations/jira/issue-reconciler.js +1 -2
  88. package/dist/integrations/jira/reconciliation-config.js +1 -2
  89. package/dist/integrations/jira/routes.js +1 -2
  90. package/dist/integrations/jira/rules.js +1 -2
  91. package/dist/integrations/linear/agent-tools.d.ts +1 -1
  92. package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
  93. package/dist/integrations/linear/agent-tools.js +5 -5
  94. package/dist/integrations/linear/agent-tools.js.map +1 -1
  95. package/dist/integrations/linear/claim.js +1 -2
  96. package/dist/integrations/linear/default-rules.js +1 -2
  97. package/dist/integrations/linear/integration.d.ts +2 -0
  98. package/dist/integrations/linear/integration.d.ts.map +1 -1
  99. package/dist/integrations/linear/integration.js +2 -2
  100. package/dist/integrations/linear/integration.js.map +1 -1
  101. package/dist/integrations/linear/issue-reconciler.js +1 -2
  102. package/dist/integrations/linear/reconciliation-config.js +1 -2
  103. package/dist/integrations/linear/routes.js +1 -2
  104. package/dist/integrations/linear/rules.js +1 -2
  105. package/dist/integrations/platform/api-client.js +1 -2
  106. package/dist/integrations/platform/connect/routes.js +1 -2
  107. package/dist/integrations/platform/github/event-worker.js +6 -2
  108. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  109. package/dist/integrations/platform/github/integration.js +1 -2
  110. package/dist/integrations/platform/gitlab/event-worker.js +1 -2
  111. package/dist/integrations/platform/gitlab/integration.js +1 -2
  112. package/dist/integrations/platform/incidentio/integration.js +1 -2
  113. package/dist/integrations/platform/jira/agent-tools.js +1 -2
  114. package/dist/integrations/platform/jira/integration.js +1 -2
  115. package/dist/integrations/platform/jira/routes.js +1 -2
  116. package/dist/integrations/platform/linear/event-worker.js +1 -2
  117. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  118. package/dist/integrations/platform/linear/integration.js +2 -2
  119. package/dist/integrations/platform/linear/integration.js.map +1 -1
  120. package/dist/integrations/pull-request-head.js +1 -2
  121. package/dist/integrations/reconciliation-config.js +1 -2
  122. package/dist/integrations/slack/channel-reply-processor.d.ts.map +1 -1
  123. package/dist/integrations/slack/channel-reply-processor.js +11 -5
  124. package/dist/integrations/slack/channel-reply-processor.js.map +1 -1
  125. package/dist/integrations/slack/channel-session-link-processor.js +1 -2
  126. package/dist/integrations/slack/connect-route.js +1 -2
  127. package/dist/integrations/slack/emoji-shortcodes.generated.js +1 -2
  128. package/dist/integrations/slack/emoji.js +1 -2
  129. package/dist/integrations/slack/feed-publisher.js +1 -2
  130. package/dist/integrations/slack/integration.d.ts +5 -0
  131. package/dist/integrations/slack/integration.d.ts.map +1 -1
  132. package/dist/integrations/slack/integration.js +13 -2
  133. package/dist/integrations/slack/integration.js.map +1 -1
  134. package/dist/integrations/slack/reply-reminder-processor.js +1 -2
  135. package/dist/integrations/slack/reply-tool.js +1 -2
  136. package/dist/integrations/slack/share-file-tool.js +1 -2
  137. package/dist/integrations/slack/slack.js +2 -3
  138. package/dist/integrations/subscription-session.js +1 -2
  139. package/dist/integrations/workos/integration.js +1 -2
  140. package/dist/mcp/disconnect.js +1 -2
  141. package/dist/mcp/events.js +2 -3
  142. package/dist/mcp/routes.js +1 -2
  143. package/dist/packages/_internals/workspace/dist/index.js +1 -2
  144. package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
  145. package/dist/platform-auth-host.js +1 -2
  146. package/dist/provider-display-name.js +1 -2
  147. package/dist/review-verdict.d.ts +1 -0
  148. package/dist/review-verdict.d.ts.map +1 -1
  149. package/dist/review-verdict.js +9 -3
  150. package/dist/review-verdict.js.map +1 -1
  151. package/dist/routes/attention-activity.js +1 -2
  152. package/dist/routes/attention-parked.js +1 -2
  153. package/dist/routes/attention-proposed.js +1 -2
  154. package/dist/routes/attention-providers.js +1 -2
  155. package/dist/routes/attention.js +1 -2
  156. package/dist/routes/automation-runs.js +1 -2
  157. package/dist/routes/config.d.ts +4 -0
  158. package/dist/routes/config.d.ts.map +1 -1
  159. package/dist/routes/config.js +7 -2
  160. package/dist/routes/config.js.map +1 -1
  161. package/dist/routes/contracts.js +1 -2
  162. package/dist/routes/custom-provider-source.js +1 -2
  163. package/dist/routes/fs.js +1 -2
  164. package/dist/routes/intake.js +1 -2
  165. package/dist/routes/knowledge.js +1 -2
  166. package/dist/routes/oauth.js +1 -2
  167. package/dist/routes/projects.js +1 -2
  168. package/dist/routes/provider-credentials.js +1 -2
  169. package/dist/routes/route.js +1 -2
  170. package/dist/routes/session-access.js +1 -2
  171. package/dist/routes/skills.js +1 -2
  172. package/dist/routes/source-control-sessions.js +5 -6
  173. package/dist/routes/source-control-settings.js +1 -2
  174. package/dist/routes/surface.js +2 -3
  175. package/dist/routes/telemetry.js +1 -2
  176. package/dist/routes/tenant-credentials.js +1 -2
  177. package/dist/routes/work-items.js +2 -3
  178. package/dist/rules/binding-context.js +1 -2
  179. package/dist/rules/decision-applicability.js +1 -2
  180. package/dist/rules/dispatch-errors.js +1 -2
  181. package/dist/rules/dispatcher.js +1 -2
  182. package/dist/rules/event-rules.js +1 -2
  183. package/dist/rules/processor.js +1 -2
  184. package/dist/rules/resolve.js +1 -2
  185. package/dist/rules/review-source-tool.js +1 -2
  186. package/dist/rules/start-coordinator.js +1 -2
  187. package/dist/rules/terminal-cleanup.js +1 -2
  188. package/dist/rules/tools.js +1 -2
  189. package/dist/rules/transition-service.js +1 -2
  190. package/dist/rules/types.d.ts +4 -1
  191. package/dist/rules/types.d.ts.map +1 -1
  192. package/dist/rules/types.js +1 -2
  193. package/dist/rules/types.js.map +1 -1
  194. package/dist/rules/validation.js +1 -2
  195. package/dist/sandbox/callback-factory-sandbox.js +1 -2
  196. package/dist/sandbox/git-ref.js +1 -2
  197. package/dist/sandbox/local-factory-sandbox.d.ts +12 -4
  198. package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -1
  199. package/dist/sandbox/local-factory-sandbox.js +32 -7
  200. package/dist/sandbox/local-factory-sandbox.js.map +1 -1
  201. package/dist/sandbox/materialization.js +1 -2
  202. package/dist/sandbox/session-retirement.js +1 -2
  203. package/dist/sandbox/session-sandbox.js +1 -2
  204. package/dist/sandbox/workdir.js +1 -2
  205. package/dist/secret-encryption.js +1 -2
  206. package/dist/server-error.js +1 -2
  207. package/dist/session/credential-policy.js +1 -2
  208. package/dist/session/default-model-hydration.js +1 -2
  209. package/dist/session/environment-repositories.js +1 -2
  210. package/dist/session/environment-state-processor.d.ts +0 -1
  211. package/dist/session/environment-state-processor.d.ts.map +1 -1
  212. package/dist/session/environment-state-processor.js +2 -3
  213. package/dist/session/environment-state-processor.js.map +1 -1
  214. package/dist/session/factory-session.js +1 -2
  215. package/dist/session/filesystem-capture.js +2 -3
  216. package/dist/session/filesystem-capture.js.map +1 -1
  217. package/dist/session/first-exec-capture.js +2 -3
  218. package/dist/session/first-exec-capture.js.map +1 -1
  219. package/dist/session/first-message-capture.js +1 -2
  220. package/dist/session/live-sessions.js +1 -2
  221. package/dist/session/memory-settings-hydration.js +1 -2
  222. package/dist/session/org-seed.js +1 -2
  223. package/dist/session/reply-credential-processor.js +1 -2
  224. package/dist/session/resource-migration.js +1 -2
  225. package/dist/session/run-audit.js +1 -2
  226. package/dist/session/session-threads.js +1 -2
  227. package/dist/session/session-title.js +1 -2
  228. package/dist/session/shell-commands.js +1 -2
  229. package/dist/session/source-control-tools.js +2 -3
  230. package/dist/session/thinking-default.js +1 -2
  231. package/dist/session/thread-session-stamp.js +1 -2
  232. package/dist/session/thread-title-mirror.js +1 -2
  233. package/dist/session/thread-title-mirror.js.map +1 -1
  234. package/dist/session/work-item-repository.js +1 -2
  235. package/dist/session-owner-signal.js +1 -2
  236. package/dist/skills/catalog.js +1 -2
  237. package/dist/skills/service.js +1 -2
  238. package/dist/spa-static.js +1 -2
  239. package/dist/state-signing.js +1 -2
  240. package/dist/storage/domains/audit/actions.js +1 -2
  241. package/dist/storage/domains/audit/actors.js +1 -2
  242. package/dist/storage/domains/audit/agent-audit.js +1 -2
  243. package/dist/storage/domains/audit/base.js +1 -2
  244. package/dist/storage/domains/audit/domain.js +1 -2
  245. package/dist/storage/domains/audit/wire.js +1 -2
  246. package/dist/storage/domains/channel-identity/base.js +1 -2
  247. package/dist/storage/domains/comments/activity.js +1 -2
  248. package/dist/storage/domains/comments/actor.js +1 -2
  249. package/dist/storage/domains/comments/base.js +1 -2
  250. package/dist/storage/domains/comments/domain.js +1 -2
  251. package/dist/storage/domains/comments/feed-context.js +1 -2
  252. package/dist/storage/domains/comments/parse.js +1 -2
  253. package/dist/storage/domains/comments/routes.js +1 -2
  254. package/dist/storage/domains/comments/schema.js +1 -2
  255. package/dist/storage/domains/comments/wire.js +1 -2
  256. package/dist/storage/domains/credentials/base.d.ts +4 -1
  257. package/dist/storage/domains/credentials/base.d.ts.map +1 -1
  258. package/dist/storage/domains/credentials/base.js +40 -44
  259. package/dist/storage/domains/credentials/base.js.map +1 -1
  260. package/dist/storage/domains/custom-providers/base.js +1 -2
  261. package/dist/storage/domains/filesystem/base.js +1 -2
  262. package/dist/storage/domains/intake/base.js +1 -2
  263. package/dist/storage/domains/integrations/base.js +1 -2
  264. package/dist/storage/domains/memory-settings/base.js +1 -2
  265. package/dist/storage/domains/model-defaults/base.js +1 -2
  266. package/dist/storage/domains/projects/base.js +1 -2
  267. package/dist/storage/domains/queue-health/base.js +1 -2
  268. package/dist/storage/domains/source-control/base.js +1 -2
  269. package/dist/storage/domains/source-control/inmemory.js +1 -2
  270. package/dist/storage/domains/work-items/base.js +1 -2
  271. package/dist/storage/domains/work-items/board-version.js +1 -2
  272. package/dist/storage/domains/work-items/metrics.js +1 -2
  273. package/dist/telemetry-types.js +1 -2
  274. package/dist/telemetry.js +1 -2
  275. package/dist/timing.js +1 -2
  276. package/dist/tools/annotations.js +1 -2
  277. package/dist/tools/approval-tools.js +1 -2
  278. package/dist/tools/deliver.js +2 -3
  279. package/dist/tools/index.js +1 -2
  280. package/dist/tools/messaging-tools.js +1 -2
  281. package/dist/tools/permissions.js +1 -2
  282. package/dist/tools/read-tools.js +1 -2
  283. package/dist/tools/reports.js +1 -2
  284. package/dist/tools/run-watch.js +1 -2
  285. package/dist/tools/sandbox-tools.js +1 -2
  286. package/dist/tools/scope.js +2 -3
  287. package/dist/tools/session-tools.js +1 -2
  288. package/dist/tools/sessions-state-processor.js +1 -2
  289. package/dist/tools/side-sessions.js +1 -2
  290. package/dist/tools/work-item-tools.js +1 -2
  291. package/dist/work-item-branch.js +1 -2
  292. package/dist/work-item-branch.js.map +1 -1
  293. package/dist/work-items/create-work-item.js +1 -2
  294. package/dist/work-items/decision-actions.js +1 -2
  295. package/dist/workspace.d.ts.map +1 -1
  296. package/dist/workspace.js +1 -3
  297. package/dist/workspace.js.map +1 -1
  298. package/factory-skills/factory-rereview/SKILL.md +12 -2
  299. package/factory-skills/factory-review/SKILL.md +12 -2
  300. package/factory-skills/factory-triage/SKILL.md +7 -7
  301. package/package.json +19 -17
@@ -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: { 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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"}
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 break;\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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"}
@@ -1,5 +1,5 @@
1
1
  import { WORKSPACE_TOOLS_PREFIX } from "@mastra/core/workspace";
2
- //#region src/session/first-exec-capture.ts
2
+ // src/session/first-exec-capture.ts
3
3
  /**
4
4
  * Tool names that count as "meaningful exec" independent of the workspace
5
5
  * prefix. The workspace tools get remapped from `mastra_workspace_*` to
@@ -79,13 +79,12 @@ function observeSessionFirstExec(session, { sourceControl }) {
79
79
  seen = true;
80
80
  unsubscribe();
81
81
  sourceControl.sessions.markFirstMeaningfulExec({ sessionId: session.identity.getId() }).catch((error) => console.warn("[Factory first-exec capture] Unable to persist first exec time.", error));
82
- return;
82
+ break;
83
83
  }
84
84
  }
85
85
  });
86
86
  return unsubscribe;
87
87
  }
88
- //#endregion
89
88
  export { isMeaningfulToolName, observeSessionFirstExec };
90
89
 
91
90
  //# sourceMappingURL=first-exec-capture.js.map
@@ -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: { 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"}
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 break;\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"}
@@ -1,4 +1,4 @@
1
- //#region src/session/first-message-capture.ts
1
+ // src/session/first-message-capture.ts
2
2
  /**
3
3
  * Record when a session's agent first produced an actual conversational
4
4
  * message (the TTFI anchor — "time to first interaction").
@@ -29,7 +29,6 @@ function observeSessionFirstMessage(session, { sourceControl }) {
29
29
  });
30
30
  return unsubscribe;
31
31
  }
32
- //#endregion
33
32
  export { observeSessionFirstMessage };
34
33
 
35
34
  //# sourceMappingURL=first-message-capture.js.map
@@ -1,4 +1,4 @@
1
- //#region src/session/live-sessions.ts
1
+ // src/session/live-sessions.ts
2
2
  /**
3
3
  * The sessions this process has materialized, by session id.
4
4
  *
@@ -84,7 +84,6 @@ var LiveSessions = class {
84
84
  return () => this.#parkedListeners.delete(listener);
85
85
  }
86
86
  };
87
- //#endregion
88
87
  export { LiveSessions };
89
88
 
90
89
  //# sourceMappingURL=live-sessions.js.map
@@ -1,5 +1,5 @@
1
1
  import { hasResolvedOrg, seedSessionOrg } from "./org-seed.js";
2
- //#region src/session/memory-settings-hydration.ts
2
+ // src/session/memory-settings-hydration.ts
3
3
  /** Default thresholds mirror the TUI `/om` fallbacks. */
4
4
  const DEFAULT_OBSERVATION_THRESHOLD = 3e4;
5
5
  const DEFAULT_REFLECTION_THRESHOLD = 4e4;
@@ -20,7 +20,6 @@ async function hydrateSessionMemorySettings(session, { sourceControl }) {
20
20
  if (!session.state.get()?.factoryOrgId) await seedSessionOrg(session, void 0);
21
21
  }
22
22
  }
23
- //#endregion
24
23
  export { DEFAULT_OBSERVATION_THRESHOLD, DEFAULT_REFLECTION_THRESHOLD, hydrateSessionMemorySettings };
25
24
 
26
25
  //# sourceMappingURL=memory-settings-hydration.js.map
@@ -1,4 +1,4 @@
1
- //#region src/session/org-seed.ts
1
+ // src/session/org-seed.ts
2
2
  /** Read the tenant org off a request context's `user` key, if there is one. */
3
3
  function readRequestContextOrgId(requestContext) {
4
4
  if (!requestContext) return void 0;
@@ -47,7 +47,6 @@ async function seedSessionOrg(session, orgId) {
47
47
  console.warn("[Factory org seed] Unable to record the session organization.", error);
48
48
  }
49
49
  }
50
- //#endregion
51
50
  export { hasResolvedOrg, readRequestContextOrgId, seedSessionOrg };
52
51
 
53
52
  //# sourceMappingURL=org-seed.js.map
@@ -1,7 +1,7 @@
1
1
  import { credentialLabel, getAuthProviderId } from "../routes/provider-credentials.js";
2
2
  import { resolveCredentialStore, resolveTenantFromRequestContext } from "@mastra/code-sdk/agents/credential-resolver";
3
3
  import { normalizeLegacyProviderId } from "@mastra/code-sdk/providers/model-ids";
4
- //#region src/session/reply-credential-processor.ts
4
+ // src/session/reply-credential-processor.ts
5
5
  /**
6
6
  * Stamps each assistant reply with the credential that answered it, as
7
7
  * `content.metadata.credentialLabel` ("Claude Pro/Max", "Anthropic API key")
@@ -32,7 +32,6 @@ var FactoryReplyCredentialProcessor = class {
32
32
  return messageList;
33
33
  }
34
34
  };
35
- //#endregion
36
35
  export { FactoryReplyCredentialProcessor };
37
36
 
38
37
  //# sourceMappingURL=reply-credential-processor.js.map
@@ -1,6 +1,6 @@
1
1
  import { externallyAuthoredWorkItem } from "../rules/types.js";
2
2
  import { sessionStampCounts } from "./session-threads.js";
3
- //#region src/session/resource-migration.ts
3
+ // src/session/resource-migration.ts
4
4
  /** Slack-started sessions live on a `slack/<thread>` branch (see `threadBranch` in the Slack integration). */
5
5
  const SLACK_SESSION_BRANCH_PREFIX = "slack/";
6
6
  /**
@@ -189,7 +189,6 @@ async function moveThread(deps, thread, resourceId) {
189
189
  }
190
190
  });
191
191
  }
192
- //#endregion
193
192
  export { migrateSessionResources, privatizeOwnerResourceSessions, sessionMemoryResource };
194
193
 
195
194
  //# sourceMappingURL=resource-migration.js.map
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- //#region src/session/run-audit.ts
2
+ // src/session/run-audit.ts
3
3
  const FACTORY_OPEN_RUNS_SETTING = "factoryOpenRuns";
4
4
  const factoryOpenRunSchema = z.object({
5
5
  kickoffId: z.string(),
@@ -209,7 +209,6 @@ function observeSessionRunEnd(session, { audit }) {
209
209
  serializeRunAudit(session, () => recordRunEnds(session, audit, event.reason, void 0, turn));
210
210
  });
211
211
  }
212
- //#endregion
213
212
  export { FACTORY_OPEN_RUNS_SETTING, FACTORY_OPEN_RUN_STALE_MS, FACTORY_RUN_USAGE_STEPS_JSON_BUDGET, heartbeatSessionOpenRun, isOpenRunLiveElsewhere, listSessionOpenRuns, observeSessionRunEnd, recordSessionRunStart, waitForSessionRunAudit };
214
213
 
215
214
  //# sourceMappingURL=run-audit.js.map
@@ -1,4 +1,4 @@
1
- //#region src/session/session-threads.ts
1
+ // src/session/session-threads.ts
2
2
  /**
3
3
  * The one memory resource a Factory session runs under. A Slack session uses
4
4
  * its Factory's, or its sender's for a DM; a side session uses its Factory's,
@@ -91,7 +91,6 @@ async function listFactorySessionThreads(controller, session, runs) {
91
91
  for (const thread of [...owned, ...legacy]) threads.set(thread.id, thread);
92
92
  return [...threads.values()];
93
93
  }
94
- //#endregion
95
94
  export { isRunBound, isWorkerSession, listFactorySessionThreads, memoryResourceOf, sessionMemoryResourceId, sessionStampCounts, sessionThreadsToMove, userSessionResourceFor };
96
95
 
97
96
  //# sourceMappingURL=session-threads.js.map
@@ -1,4 +1,4 @@
1
- //#region src/session/session-title.ts
1
+ // src/session/session-title.ts
2
2
  const MAX_SESSION_TITLE_LENGTH = 80;
3
3
  /**
4
4
  * The one shape a session row's title may take, wherever it comes from — typed
@@ -8,7 +8,6 @@ const MAX_SESSION_TITLE_LENGTH = 80;
8
8
  function normalizeSessionTitle(title) {
9
9
  return [...title.replace(/\s+/g, " ").trim()].slice(0, MAX_SESSION_TITLE_LENGTH).join("").trimEnd() || null;
10
10
  }
11
- //#endregion
12
11
  export { normalizeSessionTitle };
13
12
 
14
13
  //# sourceMappingURL=session-title.js.map
@@ -1,4 +1,4 @@
1
- //#region src/session/shell-commands.ts
1
+ // src/session/shell-commands.ts
2
2
  /** Two tool observers read the same `gh pr create`, so the reading lives here rather than once per observer. */
3
3
  const GH_PR_CREATE_RE = /(?:^|\n|;|&&|\|\|)\s*gh\s+pr\s+create(?:\s|$)/;
4
4
  /** Heredoc bodies dropped, so prose inside a document never reads as a command. */
@@ -19,7 +19,6 @@ function executableCommand(command) {
19
19
  function runsPullRequestCreate(command) {
20
20
  return GH_PR_CREATE_RE.test(executableCommand(command));
21
21
  }
22
- //#endregion
23
22
  export { executableCommand, runsPullRequestCreate };
24
23
 
25
24
  //# sourceMappingURL=shell-commands.js.map
@@ -3,11 +3,11 @@ import { mergeRequestNumberFromBranch } from "../work-item-branch.js";
3
3
  import { ensureLocalBranch, isCheckoutOf, pushRepositoryBranch, refreshMergeRequestCheckout } from "../integrations/github/sandbox.js";
4
4
  import { resolveSessionWorkdir } from "../sandbox/session-sandbox.js";
5
5
  import { updateSessionEnvironmentRepository } from "./environment-state-processor.js";
6
- import { environmentSlugs, findEnvironmentRepository, resolveSessionRepositories } from "./environment-repositories.js";
7
6
  import { normalizedVerdictLine } from "../review-verdict.js";
7
+ import { environmentSlugs, findEnvironmentRepository, resolveSessionRepositories } from "./environment-repositories.js";
8
8
  import { z } from "zod";
9
9
  import { createTool } from "@mastra/core/tools";
10
- //#region src/session/source-control-tools.ts
10
+ // src/session/source-control-tools.ts
11
11
  /** Returns why a review's leading `Verdict:` line contradicts the submitted event, if it does. */
12
12
  function verdictEventMismatch(event, body) {
13
13
  const match = /^verdict: ?(approve|request changes|changes requested)\b/.exec(normalizedVerdictLine(body) ?? "");
@@ -655,7 +655,6 @@ async function createSourceControlTools({ requestContext, providers, audit, test
655
655
  })
656
656
  };
657
657
  }
658
- //#endregion
659
658
  export { FACTORY_TEST_CHECK_NAME, createSourceControlTools, reviewedHeadFromBody, verdictEventMismatch };
660
659
 
661
660
  //# sourceMappingURL=source-control-tools.js.map
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_FACTORY_THINKING_LEVEL } from "../storage/domains/projects/base.js";
2
2
  import { UUID_PATTERN } from "../routes/contracts.js";
3
3
  import { resolveFactoryModelPolicy, routedPersonalDefaults } from "./credential-policy.js";
4
- //#region src/session/thinking-default.ts
4
+ // src/session/thinking-default.ts
5
5
  const FACTORY_PROJECT_ID = new RegExp(UUID_PATTERN);
6
6
  function sessionlessFactoryProjectId(controller) {
7
7
  const stateProjectId = (controller.getState?.())?.factoryProjectId;
@@ -31,7 +31,6 @@ async function resolveFactoryThinkingLevel(requestContext, deps) {
31
31
  return DEFAULT_FACTORY_THINKING_LEVEL;
32
32
  }
33
33
  }
34
- //#endregion
35
34
  export { resolveFactoryThinkingLevel };
36
35
 
37
36
  //# sourceMappingURL=thinking-default.js.map
@@ -1,4 +1,4 @@
1
- //#region src/session/thread-session-stamp.ts
1
+ // src/session/thread-session-stamp.ts
2
2
  /**
3
3
  * Stamp each thread a session creates with `factorySessionId`. Sessions share
4
4
  * their Factory's or user's resource, so the stamp is what ties a new thread
@@ -14,7 +14,6 @@ function observeSessionThreadStamp(session) {
14
14
  }).catch((error) => console.warn("[Factory thread stamp] Unable to tie the new thread to its session.", error));
15
15
  });
16
16
  }
17
- //#endregion
18
17
  export { observeSessionThreadStamp };
19
18
 
20
19
  //# sourceMappingURL=thread-session-stamp.js.map
@@ -1,5 +1,5 @@
1
1
  import { normalizeSessionTitle } from "./session-title.js";
2
- //#region src/session/thread-title-mirror.ts
2
+ // src/session/thread-title-mirror.ts
3
3
  /**
4
4
  * Project the bound thread's title onto its source-control session row, from
5
5
  * whichever namer produced it — core on the first turn, the observational-memory
@@ -45,7 +45,6 @@ function observeSessionThreadTitle(session, { sourceControl }) {
45
45
  }
46
46
  });
47
47
  }
48
- //#endregion
49
48
  export { observeSessionThreadTitle };
50
49
 
51
50
  //# sourceMappingURL=thread-title-mirror.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"thread-title-mirror.js","names":[],"sources":["../../src/session/thread-title-mirror.ts"],"sourcesContent":["import type { AgentControllerEvent, AgentControllerThread } from '@mastra/core/agent-controller';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { normalizeSessionTitle } from './session-title.js';\n\nexport interface ThreadTitleMirrorSession {\n readonly identity: { getId(): string };\n readonly thread: { getById(args: { threadId: string }): Promise<AgentControllerThread | null> };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface ThreadTitleMirrorDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId' | 'rename'>;\n };\n}\n\n/**\n * Project the bound thread's title onto its source-control session row, from\n * whichever namer produced it — core on the first turn, the observational-memory\n * observer as the thread grows, or an explicit rename.\n *\n * The factory sidebar reads the session row, which otherwise keeps the raw first\n * prompt (chat sessions) or nothing at all (work sessions, which then show their\n * branch). Binding a thread reconciles the row against the stored title, so a\n * session started before this ran — or one whose rename event was missed — is\n * named the next time it is opened.\n *\n * A session names threads it is no longer showing, and each write reads the row\n * before replacing it. Titles for any other thread are therefore dropped, and\n * writes run one at a time so the last event wins rather than the fastest.\n */\nexport function observeSessionThreadTitle(\n session: ThreadTitleMirrorSession,\n { sourceControl }: ThreadTitleMirrorDependencies,\n): () => void {\n const sessionId = session.identity.getId();\n let boundThreadId: string | undefined;\n let writes: Promise<void> = Promise.resolve();\n\n const project = (threadId: string, rawTitle: string | undefined): void => {\n // A thread can be named before anything binds one, so the first thread to\n // surface a title speaks for the row until `thread_changed` rebinds it.\n boundThreadId ??= threadId;\n if (threadId !== boundThreadId) return;\n const title = rawTitle ? normalizeSessionTitle(rawTitle) : null;\n if (!title) return;\n\n writes = writes\n .then(async () => {\n if (threadId !== boundThreadId) return;\n const row = await sourceControl.sessions.getBySessionId(sessionId);\n if (!row || row.title === title) return;\n await sourceControl.sessions.rename({ sessionId, title });\n })\n .catch(error => console.warn('[Factory thread-title mirror] Unable to persist the session title.', error));\n };\n\n return session.subscribe(event => {\n switch (event.type) {\n case 'thread_title_updated':\n return project(event.threadId, event.title);\n case 'om_thread_title_updated':\n return project(event.threadId, event.newTitle);\n case 'thread_changed': {\n boundThreadId = event.threadId;\n void session.thread\n .getById({ threadId: event.threadId })\n .then(thread => project(event.threadId, thread?.title))\n .catch(error => console.warn('[Factory thread-title mirror] Unable to read the bound thread.', error));\n return;\n }\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgCA,SAAgB,0BACd,SACA,EAAE,iBACU;CACZ,MAAM,YAAY,QAAQ,SAAS,MAAM;CACzC,IAAI;CACJ,IAAI,SAAwB,QAAQ,QAAQ;CAE5C,MAAM,WAAW,UAAkB,aAAuC;EAGxE,kBAAkB;EAClB,IAAI,aAAa,eAAe;EAChC,MAAM,QAAQ,WAAW,sBAAsB,QAAQ,IAAI;EAC3D,IAAI,CAAC,OAAO;EAEZ,SAAS,OACN,KAAK,YAAY;GAChB,IAAI,aAAa,eAAe;GAChC,MAAM,MAAM,MAAM,cAAc,SAAS,eAAe,SAAS;GACjE,IAAI,CAAC,OAAO,IAAI,UAAU,OAAO;GACjC,MAAM,cAAc,SAAS,OAAO;IAAE;IAAW;GAAM,CAAC;EAC1D,CAAC,CAAC,CACD,OAAM,UAAS,QAAQ,KAAK,sEAAsE,KAAK,CAAC;CAC7G;CAEA,OAAO,QAAQ,WAAU,UAAS;EAChC,QAAQ,MAAM,MAAd;GACE,KAAK,wBACH,OAAO,QAAQ,MAAM,UAAU,MAAM,KAAK;GAC5C,KAAK,2BACH,OAAO,QAAQ,MAAM,UAAU,MAAM,QAAQ;GAC/C,KAAK;IACH,gBAAgB,MAAM;IACtB,QAAa,OACV,QAAQ,EAAE,UAAU,MAAM,SAAS,CAAC,CAAC,CACrC,MAAK,WAAU,QAAQ,MAAM,UAAU,QAAQ,KAAK,CAAC,CAAC,CACtD,OAAM,UAAS,QAAQ,KAAK,kEAAkE,KAAK,CAAC;IACvG;EAEJ;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"thread-title-mirror.js","names":[],"sources":["../../src/session/thread-title-mirror.ts"],"sourcesContent":["import type { AgentControllerEvent, AgentControllerThread } from '@mastra/core/agent-controller';\n\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { normalizeSessionTitle } from './session-title.js';\n\nexport interface ThreadTitleMirrorSession {\n readonly identity: { getId(): string };\n readonly thread: { getById(args: { threadId: string }): Promise<AgentControllerThread | null> };\n subscribe(listener: (event: AgentControllerEvent) => void): () => void;\n}\n\nexport interface ThreadTitleMirrorDependencies {\n sourceControl: {\n sessions: Pick<SourceControlStorageHandle['sessions'], 'getBySessionId' | 'rename'>;\n };\n}\n\n/**\n * Project the bound thread's title onto its source-control session row, from\n * whichever namer produced it — core on the first turn, the observational-memory\n * observer as the thread grows, or an explicit rename.\n *\n * The factory sidebar reads the session row, which otherwise keeps the raw first\n * prompt (chat sessions) or nothing at all (work sessions, which then show their\n * branch). Binding a thread reconciles the row against the stored title, so a\n * session started before this ran — or one whose rename event was missed — is\n * named the next time it is opened.\n *\n * A session names threads it is no longer showing, and each write reads the row\n * before replacing it. Titles for any other thread are therefore dropped, and\n * writes run one at a time so the last event wins rather than the fastest.\n */\nexport function observeSessionThreadTitle(\n session: ThreadTitleMirrorSession,\n { sourceControl }: ThreadTitleMirrorDependencies,\n): () => void {\n const sessionId = session.identity.getId();\n let boundThreadId: string | undefined;\n let writes: Promise<void> = Promise.resolve();\n\n const project = (threadId: string, rawTitle: string | undefined): void => {\n // A thread can be named before anything binds one, so the first thread to\n // surface a title speaks for the row until `thread_changed` rebinds it.\n boundThreadId ??= threadId;\n if (threadId !== boundThreadId) return;\n const title = rawTitle ? normalizeSessionTitle(rawTitle) : null;\n if (!title) return;\n\n writes = writes\n .then(async () => {\n if (threadId !== boundThreadId) return;\n const row = await sourceControl.sessions.getBySessionId(sessionId);\n if (!row || row.title === title) return;\n await sourceControl.sessions.rename({ sessionId, title });\n })\n .catch(error => console.warn('[Factory thread-title mirror] Unable to persist the session title.', error));\n };\n\n return session.subscribe(event => {\n switch (event.type) {\n case 'thread_title_updated':\n return project(event.threadId, event.title);\n case 'om_thread_title_updated':\n return project(event.threadId, event.newTitle);\n case 'thread_changed': {\n boundThreadId = event.threadId;\n void session.thread\n .getById({ threadId: event.threadId })\n .then(thread => project(event.threadId, thread?.title))\n .catch(error => console.warn('[Factory thread-title mirror] Unable to read the bound thread.', error));\n return undefined;\n }\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgCA,SAAgB,0BACd,SACA,EAAE,iBACU;CACZ,MAAM,YAAY,QAAQ,SAAS,MAAM;CACzC,IAAI;CACJ,IAAI,SAAwB,QAAQ,QAAQ;CAE5C,MAAM,WAAW,UAAkB,aAAuC;EAGxE,kBAAkB;EAClB,IAAI,aAAa,eAAe;EAChC,MAAM,QAAQ,WAAW,sBAAsB,QAAQ,IAAI;EAC3D,IAAI,CAAC,OAAO;EAEZ,SAAS,OACN,KAAK,YAAY;GAChB,IAAI,aAAa,eAAe;GAChC,MAAM,MAAM,MAAM,cAAc,SAAS,eAAe,SAAS;GACjE,IAAI,CAAC,OAAO,IAAI,UAAU,OAAO;GACjC,MAAM,cAAc,SAAS,OAAO;IAAE;IAAW;GAAM,CAAC;EAC1D,CAAC,CAAC,CACD,OAAM,UAAS,QAAQ,KAAK,sEAAsE,KAAK,CAAC;CAC7G;CAEA,OAAO,QAAQ,WAAU,UAAS;EAChC,QAAQ,MAAM,MAAd;GACE,KAAK,wBACH,OAAO,QAAQ,MAAM,UAAU,MAAM,KAAK;GAC5C,KAAK,2BACH,OAAO,QAAQ,MAAM,UAAU,MAAM,QAAQ;GAC/C,KAAK;IACH,gBAAgB,MAAM;IACtB,QAAa,OACV,QAAQ,EAAE,UAAU,MAAM,SAAS,CAAC,CAAC,CACrC,MAAK,WAAU,QAAQ,MAAM,UAAU,QAAQ,KAAK,CAAC,CAAC,CACtD,OAAM,UAAS,QAAQ,KAAK,kEAAkE,KAAK,CAAC;IACvG;EAEJ;CACF,CAAC;AACH"}
@@ -1,5 +1,5 @@
1
1
  import { SourceControlConnectionNotFoundError } from "../storage/domains/source-control/base.js";
2
- //#region src/session/work-item-repository.ts
2
+ // src/session/work-item-repository.ts
3
3
  async function listLinkedRepositories(args) {
4
4
  const { sourceControl, orgId, factoryProjectId } = args;
5
5
  const connections = await sourceControl.connections.list({
@@ -107,7 +107,6 @@ async function resolveWorkItemRepository(args) {
107
107
  candidates
108
108
  };
109
109
  }
110
- //#endregion
111
110
  export { resolveWorkItemRepository };
112
111
 
113
112
  //# sourceMappingURL=work-item-repository.js.map
@@ -1,6 +1,5 @@
1
- //#region src/session-owner-signal.ts
1
+ // src/session-owner-signal.ts
2
2
  const SESSION_OWNER_SIGNAL_TAG = "factory-session-owner";
3
- //#endregion
4
3
  export { SESSION_OWNER_SIGNAL_TAG };
5
4
 
6
5
  //# sourceMappingURL=session-owner-signal.js.map
@@ -2,7 +2,7 @@ import { BUNDLED_FACTORY_SKILLS_PATH, FACTORY_SKILL_NAMES, resolveLocalFactorySk
2
2
  import { join } from "path";
3
3
  import { readFile } from "fs/promises";
4
4
  import { validateSkillContent } from "@mastra/core/skills";
5
- //#region src/skills/catalog.ts
5
+ // src/skills/catalog.ts
6
6
  /**
7
7
  * Read-only catalog of the Factory skills.
8
8
  *
@@ -56,7 +56,6 @@ async function listFactorySkills() {
56
56
  }
57
57
  return skills;
58
58
  }
59
- //#endregion
60
59
  export { listFactorySkills };
61
60
 
62
61
  //# sourceMappingURL=catalog.js.map
@@ -1,6 +1,6 @@
1
1
  import { REVIEW_ONLY_FACTORY_SKILLS, rescanFactorySkills } from "../workspace.js";
2
2
  import { formatSkillActivation } from "@mastra/core/workspace";
3
- //#region src/skills/service.ts
3
+ // src/skills/service.ts
4
4
  var SkillInvocationError = class extends Error {
5
5
  code;
6
6
  constructor(code, message) {
@@ -72,7 +72,6 @@ async function resolveSkillResumeInvocation(controller, input) {
72
72
  contents
73
73
  };
74
74
  }
75
- //#endregion
76
75
  export { SkillInvocationError, formatSkillMessage, resolvePromptInvocation, resolveSkillInvocation, resolveSkillResumeInvocation };
77
76
 
78
77
  //# sourceMappingURL=service.js.map
@@ -2,7 +2,7 @@ import { existsSync } from "fs";
2
2
  import { dirname, extname, join, normalize, resolve, sep } from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import { readFile, stat } from "fs/promises";
5
- //#region src/spa-static.ts
5
+ // src/spa-static.ts
6
6
  /**
7
7
  * Same-origin SPA serving for production.
8
8
  *
@@ -98,7 +98,6 @@ function createSpaStaticMiddleware(uiDist) {
98
98
  return next();
99
99
  };
100
100
  }
101
- //#endregion
102
101
  export { createSpaStaticMiddleware, resolveUiDistDir };
103
102
 
104
103
  //# sourceMappingURL=spa-static.js.map
@@ -1,5 +1,5 @@
1
1
  import { createHmac, timingSafeEqual } from "crypto";
2
- //#region src/state-signing.ts
2
+ // src/state-signing.ts
3
3
  /**
4
4
  * Shared OAuth/install `state` signing for web integrations.
5
5
  *
@@ -77,7 +77,6 @@ function createStateSigner(secret) {
77
77
  }
78
78
  };
79
79
  }
80
- //#endregion
81
80
  export { createStateSigner };
82
81
 
83
82
  //# sourceMappingURL=state-signing.js.map
@@ -1,4 +1,4 @@
1
- //#region src/storage/domains/audit/actions.ts
1
+ // src/storage/domains/audit/actions.ts
2
2
  /**
3
3
  * Every action the trail can hold, by namespace. Register new ones in the
4
4
  * WorkOS dashboard under Audit Logs → Events before the mirror accepts them.
@@ -64,7 +64,6 @@ function isAuditAction(value) {
64
64
  function auditActionsInNamespaces(namespaces) {
65
65
  return namespaces.flatMap((namespace) => AUDIT_ACTIONS[namespace].map((leaf) => `factory.${namespace}.${leaf}`));
66
66
  }
67
- //#endregion
68
67
  export { AUDIT_ACTIONS, auditActionsInNamespaces, auditNamespaces, isAuditAction, isAuditNamespace, parseAuditAction };
69
68
 
70
69
  //# sourceMappingURL=actions.js.map
@@ -1,4 +1,4 @@
1
- //#region src/storage/domains/audit/actors.ts
1
+ // src/storage/domains/audit/actors.ts
2
2
  const AUDIT_ACTOR_TYPES = [
3
3
  "human",
4
4
  "agent",
@@ -19,7 +19,6 @@ function isHumanActorId(actorId) {
19
19
  if (!actorId) return false;
20
20
  return !AUTOMATION_ACTORS.has(actorId) && !actorId.startsWith("agent:") && !actorId.startsWith("github:");
21
21
  }
22
- //#endregion
23
22
  export { AUDIT_ACTOR_TYPES, isAuditActorType, isHumanActorId };
24
23
 
25
24
  //# sourceMappingURL=actors.js.map
@@ -1,5 +1,5 @@
1
1
  import { executableCommand, runsPullRequestCreate } from "../../../session/shell-commands.js";
2
- //#region src/storage/domains/audit/agent-audit.ts
2
+ // src/storage/domains/audit/agent-audit.ts
3
3
  /** Match command-start positions while ignoring command text embedded in heredoc bodies. */
4
4
  const GIT_COMMIT_RE = /(?:^|\n|;|&&|\|\|)\s*git\s+commit(?:\s|$)/;
5
5
  const GIT_PUSH_RE = /(?:^|\n|;|&&|\|\|)\s*git\s+push(?:\s|$)/;
@@ -87,7 +87,6 @@ async function observeAgentGitAction({ audit, toolContext, resolveRepositorySlug
87
87
  console.warn("[Audit] Failed to observe agent git action", { error: err instanceof Error ? err.message : String(err) });
88
88
  }
89
89
  }
90
- //#endregion
91
90
  export { observeAgentGitAction };
92
91
 
93
92
  //# sourceMappingURL=agent-audit.js.map
@@ -1,5 +1,5 @@
1
1
  import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage";
2
- //#region src/storage/domains/audit/base.ts
2
+ // src/storage/domains/audit/base.ts
3
3
  /**
4
4
  * Factory audit events domain — the append-only "who did what, when" trail
5
5
  * behind the software factory.
@@ -218,7 +218,6 @@ var AuditStorage = class extends FactoryStorageDomain {
218
218
  };
219
219
  }
220
220
  };
221
- //#endregion
222
221
  export { ACTOR_PROFILE_METADATA_KEY, AUDIT_EVENTS_SCHEMA, AuditStorage, auditActorProfile, auditCursorAt, boundAuditMetadata, clampAuditLimit, decodeAuditAfterCursor, decodeAuditCursor, encodeAuditCursor };
223
222
 
224
223
  //# sourceMappingURL=base.js.map
@@ -4,7 +4,7 @@ import { auditActionsInNamespaces, isAuditNamespace } from "./actions.js";
4
4
  import { isHumanActorId } from "./actors.js";
5
5
  import { toWireAuditEvent } from "./wire.js";
6
6
  import { registerApiRoute } from "@mastra/core/server";
7
- //#region src/storage/domains/audit/domain.ts
7
+ // src/storage/domains/audit/domain.ts
8
8
  function readStoredActorProfile(metadata) {
9
9
  if (!metadata) return void 0;
10
10
  const raw = metadata[ACTOR_PROFILE_METADATA_KEY];
@@ -242,7 +242,6 @@ var AuditDomain = class {
242
242
  };
243
243
  }
244
244
  };
245
- //#endregion
246
245
  export { AuditDomain, auditRequestContext, auditRequestOrigin };
247
246
 
248
247
  //# sourceMappingURL=domain.js.map
@@ -1,4 +1,4 @@
1
- //#region src/storage/domains/audit/wire.ts
1
+ // src/storage/domains/audit/wire.ts
2
2
  function toWireAuditEvent(row) {
3
3
  return {
4
4
  id: row.id,
@@ -10,7 +10,6 @@ function toWireAuditEvent(row) {
10
10
  occurredAt: row.occurredAt.toISOString()
11
11
  };
12
12
  }
13
- //#endregion
14
13
  export { toWireAuditEvent };
15
14
 
16
15
  //# sourceMappingURL=wire.js.map
@@ -1,5 +1,5 @@
1
1
  import { FactoryStorageDomain } from "@mastra/core/storage";
2
- //#region src/storage/domains/channel-identity/base.ts
2
+ // src/storage/domains/channel-identity/base.ts
3
3
  const CHANNEL_ACCOUNT_LINKS_SCHEMA = {
4
4
  name: "channel_account_links",
5
5
  columns: {
@@ -158,7 +158,6 @@ var ChannelIdentityStorage = class extends FactoryStorageDomain {
158
158
  }) > 0;
159
159
  }
160
160
  };
161
- //#endregion
162
161
  export { CHANNEL_ACCOUNT_LINKS_SCHEMA, ChannelIdentityStorage };
163
162
 
164
163
  //# sourceMappingURL=base.js.map
@@ -1,6 +1,6 @@
1
1
  import { isMentionableActorId } from "./actor.js";
2
2
  import { UniqueViolationError } from "@mastra/core/storage";
3
- //#region src/storage/domains/comments/activity.ts
3
+ // src/storage/domains/comments/activity.ts
4
4
  /** The actor rule-materialized work items are created by; a phantom, never a participant. */
5
5
  const RULE_DISPATCHER_ACTOR_ID = "factory-rule-dispatcher";
6
6
  /** Ceiling: an author silent for this many comments stops receiving; a real participants table is the upgrade. */
@@ -88,7 +88,6 @@ async function listActivityForUser(ops, { orgId, factoryProjectId, userId, befor
88
88
  ...before ? { cursor: { values: [before.occurredAt, before.id] } } : {}
89
89
  })).map(toWorkItemActivity);
90
90
  }
91
- //#endregion
92
91
  export { PARTICIPANT_SCAN_LIMIT, fanOutWorkItemActivity, listActivityForUser };
93
92
 
94
93
  //# sourceMappingURL=activity.js.map