@mastra/factory 0.24.0-alpha.9 → 0.25.0-alpha.0
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.
- package/README.md +40 -13
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +18 -4
- package/dist/auth.js.map +1 -1
- package/dist/boards/review.d.ts.map +1 -1
- package/dist/boards/review.js +3 -2
- package/dist/boards/review.js.map +1 -1
- package/dist/environment/build-runner.js +2 -1
- package/dist/environment/build-runner.js.map +1 -1
- package/dist/environment/build-workflow.js +1 -1
- package/dist/environment/build-workflow.js.map +1 -1
- package/dist/environment/build.js +16 -14
- package/dist/environment/build.js.map +1 -1
- package/dist/factory.d.ts +3 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +172 -88
- package/dist/factory.js.map +1 -1
- package/dist/integrations/base.d.ts +15 -2
- package/dist/integrations/base.d.ts.map +1 -1
- package/dist/integrations/github/app-identity.js.map +1 -1
- package/dist/integrations/github/integration.js +1 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/reconcile-worker.js +1 -1
- package/dist/integrations/github/reconcile-worker.js.map +1 -1
- package/dist/integrations/github/routes.d.ts +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +24 -13
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.d.ts.map +1 -1
- package/dist/integrations/github/rules.js +53 -9
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.js +2 -2
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +1 -2
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.d.ts +7 -4
- package/dist/integrations/github/subscriptions.d.ts.map +1 -1
- package/dist/integrations/github/subscriptions.js +5 -5
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/gitlab/api.js.map +1 -1
- package/dist/integrations/gitlab/integration.js.map +1 -1
- package/dist/integrations/gitlab/reconciliation-config.js +2 -1
- package/dist/integrations/gitlab/reconciliation-config.js.map +1 -1
- package/dist/integrations/gitlab/routes.d.ts.map +1 -1
- package/dist/integrations/gitlab/routes.js +10 -6
- package/dist/integrations/gitlab/routes.js.map +1 -1
- package/dist/integrations/gitlab/rules.js.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/session-subscriptions.js +1 -2
- package/dist/integrations/gitlab/session-subscriptions.js.map +1 -1
- package/dist/integrations/gitlab/subscriptions.d.ts +7 -4
- package/dist/integrations/gitlab/subscriptions.d.ts.map +1 -1
- package/dist/integrations/gitlab/subscriptions.js +5 -5
- package/dist/integrations/gitlab/subscriptions.js.map +1 -1
- package/dist/integrations/incidentio/api.js.map +1 -1
- package/dist/integrations/incidentio/integration.js.map +1 -1
- package/dist/integrations/incidentio/rules.js.map +1 -1
- package/dist/integrations/issue-reconcile-worker.js +1 -1
- package/dist/integrations/issue-reconcile-worker.js.map +1 -1
- package/dist/integrations/issue-reconciler.js +1 -1
- package/dist/integrations/issue-reconciler.js.map +1 -1
- package/dist/integrations/jira/api.js.map +1 -1
- package/dist/integrations/jira/integration.js.map +1 -1
- package/dist/integrations/jira/rules.js.map +1 -1
- package/dist/integrations/linear/agent-tools.d.ts.map +1 -1
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +1 -1
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.js +2 -2
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/gitlab/event-worker.js.map +1 -1
- package/dist/integrations/platform/gitlab/integration.js.map +1 -1
- package/dist/integrations/platform/incidentio/integration.js +2 -1
- package/dist/integrations/platform/incidentio/integration.js.map +1 -1
- package/dist/integrations/platform/jira/integration.js.map +1 -1
- package/dist/integrations/platform/linear/event-worker.js +1 -1
- package/dist/integrations/platform/linear/event-worker.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/{session → integrations/slack}/channel-reply-processor.d.ts +11 -9
- package/dist/integrations/slack/channel-reply-processor.d.ts.map +1 -0
- package/dist/integrations/slack/channel-reply-processor.js +61 -0
- package/dist/integrations/slack/channel-reply-processor.js.map +1 -0
- package/dist/integrations/slack/channel-session-link-processor.d.ts.map +1 -0
- package/dist/{session → integrations/slack}/channel-session-link-processor.js +14 -4
- package/dist/integrations/slack/channel-session-link-processor.js.map +1 -0
- package/dist/integrations/slack/connect-route.js +1 -1
- package/dist/integrations/slack/connect-route.js.map +1 -1
- package/dist/integrations/slack/feed-publisher.js +1 -1
- package/dist/integrations/slack/feed-publisher.js.map +1 -1
- package/dist/integrations/slack/integration.d.ts +25 -1
- package/dist/integrations/slack/integration.d.ts.map +1 -1
- package/dist/integrations/slack/integration.js +56 -6
- package/dist/integrations/slack/integration.js.map +1 -1
- package/dist/integrations/slack/reply-reminder-processor.d.ts +23 -0
- package/dist/integrations/slack/reply-reminder-processor.d.ts.map +1 -0
- package/dist/integrations/slack/reply-reminder-processor.js +58 -0
- package/dist/integrations/slack/reply-reminder-processor.js.map +1 -0
- package/dist/integrations/slack/reply-tool.d.ts +31 -0
- package/dist/integrations/slack/reply-tool.d.ts.map +1 -0
- package/dist/integrations/slack/reply-tool.js +203 -0
- package/dist/integrations/slack/reply-tool.js.map +1 -0
- package/dist/integrations/slack/share-file-tool.d.ts +45 -0
- package/dist/integrations/slack/share-file-tool.d.ts.map +1 -0
- package/dist/integrations/slack/share-file-tool.js +106 -0
- package/dist/integrations/slack/share-file-tool.js.map +1 -0
- package/dist/integrations/slack/slack.d.ts +16 -5
- package/dist/integrations/slack/slack.d.ts.map +1 -1
- package/dist/integrations/slack/slack.js +141 -40
- package/dist/integrations/slack/slack.js.map +1 -1
- package/dist/integrations/subscription-session.d.ts +0 -1
- package/dist/integrations/subscription-session.d.ts.map +1 -1
- package/dist/integrations/subscription-session.js +1 -4
- package/dist/integrations/subscription-session.js.map +1 -1
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/mcp/events.d.ts +73 -0
- package/dist/mcp/events.d.ts.map +1 -0
- package/dist/mcp/events.js +158 -0
- package/dist/mcp/events.js.map +1 -0
- package/dist/mcp/routes.d.ts +0 -1
- package/dist/mcp/routes.d.ts.map +1 -1
- package/dist/mcp/routes.js +31 -4
- package/dist/mcp/routes.js.map +1 -1
- package/dist/packages/_internals/workspace/dist/index.js +18 -1
- package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
- package/dist/platform-auth-host.d.ts.map +1 -1
- package/dist/platform-auth-host.js +1 -1
- package/dist/platform-auth-host.js.map +1 -1
- package/dist/routes/attention-activity.js.map +1 -1
- package/dist/routes/attention-parked.js.map +1 -1
- package/dist/routes/attention-providers.js.map +1 -1
- package/dist/routes/automation-runs.js.map +1 -1
- package/dist/routes/config.d.ts +8 -7
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +70 -28
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/contracts.d.ts +2 -0
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/contracts.js +2 -2
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.d.ts +31 -23
- package/dist/routes/fs.d.ts.map +1 -1
- package/dist/routes/fs.js +218 -64
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +2 -1
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/knowledge.js +1 -1
- package/dist/routes/knowledge.js.map +1 -1
- package/dist/routes/oauth.js +3 -2
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.d.ts +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +33 -31
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.d.ts.map +1 -1
- package/dist/routes/provider-credentials.js +6 -1
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/session-access.d.ts +60 -0
- package/dist/routes/session-access.d.ts.map +1 -0
- package/dist/routes/session-access.js +202 -0
- package/dist/routes/session-access.js.map +1 -0
- package/dist/routes/skills.d.ts +2 -4
- package/dist/routes/skills.d.ts.map +1 -1
- package/dist/routes/skills.js +23 -68
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +8 -1
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +239 -19
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +7 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +20 -8
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +11 -9
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +21 -13
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts +2 -2
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +3 -3
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/start-coordinator.d.ts +4 -0
- package/dist/rules/start-coordinator.d.ts.map +1 -1
- package/dist/rules/start-coordinator.js +36 -4
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +2 -2
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/sandbox/callback-factory-sandbox.js.map +1 -1
- package/dist/sandbox/local-factory-sandbox.js.map +1 -1
- package/dist/sandbox/session-retirement.d.ts +11 -0
- package/dist/sandbox/session-retirement.d.ts.map +1 -1
- package/dist/sandbox/session-retirement.js +38 -1
- package/dist/sandbox/session-retirement.js.map +1 -1
- package/dist/sandbox/session-sandbox.d.ts +2 -0
- package/dist/sandbox/session-sandbox.d.ts.map +1 -1
- package/dist/sandbox/session-sandbox.js +5 -1
- package/dist/sandbox/session-sandbox.js.map +1 -1
- package/dist/secret-encryption.d.ts.map +1 -1
- package/dist/secret-encryption.js +65 -25
- package/dist/secret-encryption.js.map +1 -1
- package/dist/session/credential-policy.d.ts +4 -2
- package/dist/session/credential-policy.d.ts.map +1 -1
- package/dist/session/credential-policy.js +18 -7
- package/dist/session/credential-policy.js.map +1 -1
- package/dist/session/default-model-hydration.d.ts +1 -0
- package/dist/session/default-model-hydration.d.ts.map +1 -1
- package/dist/session/default-model-hydration.js +3 -2
- package/dist/session/default-model-hydration.js.map +1 -1
- package/dist/session/environment-state-processor.js +1 -1
- package/dist/session/environment-state-processor.js.map +1 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +14 -1
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/filesystem-capture.d.ts +3 -3
- package/dist/session/filesystem-capture.d.ts.map +1 -1
- package/dist/session/filesystem-capture.js +16 -17
- package/dist/session/filesystem-capture.js.map +1 -1
- package/dist/session/first-exec-capture.d.ts +1 -1
- package/dist/session/first-exec-capture.d.ts.map +1 -1
- package/dist/session/first-exec-capture.js +1 -1
- package/dist/session/first-exec-capture.js.map +1 -1
- package/dist/session/first-message-capture.d.ts +1 -1
- package/dist/session/first-message-capture.d.ts.map +1 -1
- package/dist/session/first-message-capture.js +1 -1
- package/dist/session/first-message-capture.js.map +1 -1
- package/dist/session/live-sessions.d.ts +1 -1
- package/dist/session/live-sessions.js +1 -1
- package/dist/session/live-sessions.js.map +1 -1
- package/dist/session/memory-settings-hydration.d.ts +1 -1
- package/dist/session/memory-settings-hydration.d.ts.map +1 -1
- package/dist/session/memory-settings-hydration.js +2 -1
- package/dist/session/memory-settings-hydration.js.map +1 -1
- package/dist/session/resource-migration.d.ts +105 -0
- package/dist/session/resource-migration.d.ts.map +1 -0
- package/dist/session/resource-migration.js +195 -0
- package/dist/session/resource-migration.js.map +1 -0
- package/dist/session/run-audit.d.ts +1 -0
- package/dist/session/run-audit.d.ts.map +1 -1
- package/dist/session/run-audit.js +7 -2
- package/dist/session/run-audit.js.map +1 -1
- package/dist/session/session-threads.d.ts +63 -0
- package/dist/session/session-threads.d.ts.map +1 -0
- package/dist/session/session-threads.js +97 -0
- package/dist/session/session-threads.js.map +1 -0
- package/dist/session/source-control-tools.js +8 -8
- package/dist/session/source-control-tools.js.map +1 -1
- package/dist/session/thinking-default.d.ts +6 -13
- package/dist/session/thinking-default.d.ts.map +1 -1
- package/dist/session/thinking-default.js +19 -24
- package/dist/session/thinking-default.js.map +1 -1
- package/dist/session/thread-session-stamp.d.ts +21 -0
- package/dist/session/thread-session-stamp.d.ts.map +1 -0
- package/dist/session/thread-session-stamp.js +20 -0
- package/dist/session/thread-session-stamp.js.map +1 -0
- package/dist/session/thread-title-mirror.d.ts +1 -1
- package/dist/session/thread-title-mirror.d.ts.map +1 -1
- package/dist/session/thread-title-mirror.js +1 -1
- package/dist/session/thread-title-mirror.js.map +1 -1
- package/dist/session-owner-signal.d.ts +2 -0
- package/dist/session-owner-signal.d.ts.map +1 -0
- package/dist/session-owner-signal.js +6 -0
- package/dist/session-owner-signal.js.map +1 -0
- package/dist/skills/service.d.ts +11 -14
- package/dist/skills/service.d.ts.map +1 -1
- package/dist/skills/service.js +12 -22
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +1 -1
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/actions.d.ts +1 -0
- package/dist/storage/domains/audit/actions.d.ts.map +1 -1
- package/dist/storage/domains/audit/actions.js +2 -1
- package/dist/storage/domains/audit/actions.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +2 -2
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.d.ts +18 -2
- package/dist/storage/domains/audit/base.d.ts.map +1 -1
- package/dist/storage/domains/audit/base.js +22 -4
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/channel-identity/base.js.map +1 -1
- package/dist/storage/domains/comments/base.js.map +1 -1
- package/dist/storage/domains/comments/domain.js.map +1 -1
- package/dist/storage/domains/comments/feed-context.js.map +1 -1
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.d.ts +8 -0
- package/dist/storage/domains/integrations/base.d.ts.map +1 -1
- package/dist/storage/domains/integrations/base.js +7 -0
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-defaults/base.d.ts +8 -0
- package/dist/storage/domains/model-defaults/base.d.ts.map +1 -1
- package/dist/storage/domains/model-defaults/base.js +18 -0
- package/dist/storage/domains/model-defaults/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +44 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +109 -3
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +23 -1
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +35 -0
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +14 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +25 -1
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/board-version.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/tools/approval-tools.js +1 -1
- package/dist/tools/approval-tools.js.map +1 -1
- package/dist/tools/deliver.d.ts +8 -5
- package/dist/tools/deliver.d.ts.map +1 -1
- package/dist/tools/deliver.js +22 -8
- package/dist/tools/deliver.js.map +1 -1
- package/dist/tools/messaging-tools.d.ts +1 -1
- package/dist/tools/messaging-tools.d.ts.map +1 -1
- package/dist/tools/messaging-tools.js +203 -72
- package/dist/tools/messaging-tools.js.map +1 -1
- package/dist/tools/permissions.d.ts +1 -1
- package/dist/tools/permissions.d.ts.map +1 -1
- package/dist/tools/permissions.js +3 -2
- package/dist/tools/permissions.js.map +1 -1
- package/dist/tools/reports.d.ts.map +1 -1
- package/dist/tools/reports.js +10 -3
- package/dist/tools/reports.js.map +1 -1
- package/dist/tools/run-watch.d.ts +2 -0
- package/dist/tools/run-watch.d.ts.map +1 -1
- package/dist/tools/run-watch.js +42 -2
- package/dist/tools/run-watch.js.map +1 -1
- package/dist/tools/sandbox-tools.d.ts +12 -0
- package/dist/tools/sandbox-tools.d.ts.map +1 -0
- package/dist/tools/sandbox-tools.js +40 -0
- package/dist/tools/sandbox-tools.js.map +1 -0
- package/dist/tools/scope.d.ts +2 -1
- package/dist/tools/scope.d.ts.map +1 -1
- package/dist/tools/scope.js +5 -7
- package/dist/tools/scope.js.map +1 -1
- package/dist/tools/session-tools.d.ts +1 -1
- package/dist/tools/session-tools.d.ts.map +1 -1
- package/dist/tools/session-tools.js +39 -18
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/sessions-state-processor.js +2 -2
- package/dist/tools/sessions-state-processor.js.map +1 -1
- package/dist/tools/side-sessions.d.ts +1 -1
- package/dist/tools/side-sessions.d.ts.map +1 -1
- package/dist/tools/side-sessions.js +1 -1
- package/dist/tools/side-sessions.js.map +1 -1
- package/dist/work-item-branch.d.ts +11 -3
- package/dist/work-item-branch.d.ts.map +1 -1
- package/dist/work-item-branch.js +34 -9
- package/dist/work-item-branch.js.map +1 -1
- package/dist/workspace.d.ts +7 -3
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +32 -17
- package/dist/workspace.js.map +1 -1
- package/factory-skills/configure-factory-rules/SKILL.md +2 -2
- package/package.json +16 -15
- package/dist/session/channel-reply-processor.d.ts.map +0 -1
- package/dist/session/channel-reply-processor.js +0 -46
- package/dist/session/channel-reply-processor.js.map +0 -1
- package/dist/session/channel-session-link-processor.d.ts.map +0 -1
- package/dist/session/channel-session-link-processor.js.map +0 -1
- /package/dist/{session → integrations/slack}/channel-session-link-processor.d.ts +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transition-service.js","names":["#configVersion","#boards","#storage","#timeoutMs","#onTerminalStage","#terminalCleanupTimeoutMs","#onAccepted","#onTransitionCommitted","#autoApprovePlans","#selfReviewEnabled","#audit","#commitRejection","#recordTransition","#evaluateAndCommit","#notifyCommitted","#commit"],"sources":["../../src/rules/transition-service.ts"],"sourcesContent":["import { createBoardRegistry } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport { boardTransitionPolicyResultSchema, immutablePolicySnapshot } from '../boards/transition-policy.js';\nimport type { AuditActorProfileInput, AuditActorType, AuditContext } from '../storage/domains/audit/base.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport { isAgentActor } from '../storage/domains/work-items/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { resolveFactoryStageRules } from './resolve.js';\nimport type {\n FactoryCommitDecision,\n FactoryRuleActor,\n FactoryRuleBoard,\n FactoryRuleCausalEntry,\n FactoryRuleRejectionCode,\n FactoryRuleStage,\n FactoryTriageType,\n FactoryStageRuleContext,\n FactoryTransitionResult,\n} from './types.js';\nimport {\n externallyAuthoredWorkItem,\n factoryRuleSourceForWorkItem,\n isFactoryRuleStage,\n workItemSource,\n} from './types.js';\nimport {\n MAX_FACTORY_RULE_CAUSAL_DEPTH,\n assertFactoryDecisionTarget,\n validateFactoryRuleDecision,\n validateFactoryRuleDecisions,\n} from './validation.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\nconst MAX_REJECTION_REASON = 512;\n/** Longest a committed transition waits for terminal resource cleanup. Cleanup\n * reattaches remote sandboxes, so a hung provider call must not leave the\n * already-committed transition request pending; past this bound the cleanup\n * keeps running in the background as pure best-effort. */\nconst TERMINAL_CLEANUP_TIMEOUT_MS = 30_000;\n\nexport interface FactoryTransitionRequest {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n /** Installed board id; the service rejects boards that are not installed. */\n board: string;\n stage: FactoryRuleStage;\n expectedRevision: number;\n actor: FactoryRuleActor;\n actorProfile?: AuditActorProfileInput;\n /** Where a browser request came from; rules and agents carry none. */\n context?: AuditContext;\n ingress: { type: 'human' | 'agent' | 'toolResult' | 'github' | 'rule'; identity: string; transitionId?: string };\n cause: string;\n causalChain?: readonly FactoryRuleCausalEntry[];\n /** Internal materialization path: evaluate only the destination onEnter leaf even when already at that stage. */\n initialEntry?: boolean;\n /** Re-runs the stage's entry rules when the item already holds that stage, to restart work the entry invalidated. */\n reenter?: boolean;\n /** Structured verdict required from a bound triage-agent terminal request. */\n triageType?: FactoryTriageType;\n /** Internal proof that this move consumes an approved `submit_plan` result. */\n planApproved?: true;\n}\n\nexport interface FactoryTransitionServiceOptions {\n configVersion: string;\n storage: WorkItemsStorage;\n boards?: BoardRegistry;\n /** Every commit, accepted or rejected, lands here as `stage_moved` / `transition_rejected` under the request's actor. */\n audit?: AuditRecorder;\n timeoutMs?: number;\n /**\n * Called after a transition commits into a phase the board declares\n * terminal — the point where the item's sessions stop receiving runs, so\n * resources they hold (e.g. sandboxes) can be released for reuse. Awaited,\n * but failures are swallowed: releasing resources must never break or roll\n * back the committed transition.\n */\n onTerminalStage?: (args: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n stage: FactoryRuleStage;\n revision: number;\n /** The actor that committed this terminal transition. Lets cleanup leave\n * the seat that drove its own transition (an agent tool call) untouched. */\n actor: FactoryRuleActor;\n }) => Promise<void> | void;\n /**\n * Called after a transition commits a stage change, e.g. to tell the item's\n * followers. Not awaited, and failures are logged: a notification must never\n * fail or delay the committed transition.\n */\n onTransitionCommitted?: (args: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n fromStage: string | undefined;\n toStage: string;\n }) => Promise<void> | void;\n /** Upper bound on how long a committed transition waits for\n * `onTerminalStage` before returning (default 30s). The cleanup continues\n * in the background past the bound. */\n terminalCleanupTimeoutMs?: number;\n /**\n * Called after a transition first records a person's acceptance of a\n * non-bug item (see `WorkItemRow.acceptedAt`). Fire-and-forget: failures\n * are swallowed, the committed transition never depends on it.\n */\n onAccepted?: (args: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n item: WorkItemRow;\n }) => Promise<void> | void;\n /**\n * Resolves whether a project auto-approves produced plans. Unset means off:\n * a plan without explicit approval is a plan a person must review.\n */\n autoApprovePlans?: (tenant: { orgId: string; factoryProjectId: string }) => Promise<boolean>;\n /** Resolves whether a project self-reviews Work items before the change request opens. Unset means off. */\n selfReviewEnabled?: (tenant: { orgId: string; factoryProjectId: string }) => Promise<boolean>;\n}\n\nfunction rejection(\n transitionId: string,\n itemId: string,\n code: FactoryRuleRejectionCode,\n reason: string,\n): FactoryTransitionResult {\n return { status: 'rejected', transitionId, itemId, code, reason: reason.slice(0, MAX_REJECTION_REASON) };\n}\n\nfunction actorId(actor: FactoryRuleActor): string {\n switch (actor.type) {\n case 'human':\n case 'system':\n return actor.id;\n case 'agent':\n return `agent:${actor.bindingId}`;\n case 'github':\n return `github:${actor.login}`;\n case 'gitlab':\n return `gitlab:${actor.username}`;\n }\n}\n\n// The dispatcher executes an agent-approved decision as a human actor so its consent carries; the trail still names the agent.\nexport function auditActorOf(actor: FactoryRuleActor): { actorId: string; actorType: AuditActorType } {\n const id = actorId(actor);\n switch (actor.type) {\n case 'github':\n case 'gitlab':\n return { actorId: id, actorType: 'human' };\n case 'human':\n return { actorId: id, actorType: isAgentActor(id) ? 'agent' : 'human' };\n default:\n return { actorId: id, actorType: actor.type };\n }\n}\n\nexport function currentStage(stages: readonly string[]): FactoryRuleStage | undefined {\n if (stages.length !== 1) return undefined;\n const stage = stages[0];\n return isFactoryRuleStage(stage) ? stage : undefined;\n}\n\ninterface TransitionConsentOptions {\n autonomy?: 'arm' | 'disarm';\n consentedBy?: string;\n accept?: boolean;\n triageType?: FactoryTriageType;\n}\n\n// Entering a resting lane disarms whoever rests it; only a person's move into a working lane arms.\nfunction transitionConsent(working: boolean, humanMove: boolean): 'arm' | 'disarm' | undefined {\n if (!working) return 'disarm';\n return humanMove ? 'arm' : undefined;\n}\n\n// An event arriving as data (GitHub, sweeps) never pre-approves the runs its transition queues.\nfunction bearsConsent(actor: FactoryRuleActor): boolean {\n return actor.type === 'human' || actor.type === 'agent';\n}\n\n// Rides the transition's own revision-checked commit, so a stale or rejected commit flips nothing.\nfunction consentEffect(\n request: FactoryTransitionRequest,\n working: boolean,\n humanMove: boolean,\n): TransitionConsentOptions {\n const autonomy = transitionConsent(working, humanMove);\n return bearsConsent(request.actor) ? { autonomy, consentedBy: actorId(request.actor) } : { autonomy };\n}\n\ntype RunStartDecision = Extract<FactoryCommitDecision, { type: 'invokeSkill' | 'sendMessage' }>;\n\nfunction startsRun(decision: FactoryCommitDecision): decision is RunStartDecision {\n return decision.type === 'invokeSkill' || (decision.type === 'sendMessage' && decision.prepareBinding === true);\n}\n\n// Answering a recorded run start, or the role's own mid-run agent, with a run would start a second one.\nfunction runAlreadyUnderway(request: FactoryTransitionRequest, decision: RunStartDecision): boolean {\n if (request.cause === 'run_start') return true;\n return request.actor.type === 'agent' && request.actor.role === decision.role;\n}\n\nfunction stageTransitionMessage(fromStage: FactoryRuleStage, toStage: FactoryRuleStage): string {\n return `This work was moved from the ${fromStage} stage to the ${toStage} stage.`;\n}\n\nfunction isTriageAgent(actor: FactoryRuleActor): actor is Extract<FactoryRuleActor, { type: 'agent' }> {\n return actor.type === 'agent' && actor.role === 'triage';\n}\n\nfunction isHumanTransition(request: FactoryTransitionRequest): boolean {\n return request.actor.type === 'human' && request.ingress.type === 'human';\n}\n\nfunction ruleFailure(error: unknown): { code: FactoryRuleRejectionCode; reason: string } {\n return {\n code: 'rule_error',\n reason: error instanceof Error ? `Factory rule failed: ${error.message}` : 'Factory rule failed.',\n };\n}\n\nasync function withRuleTimeout<T>(operation: Promise<T>, timeoutMs: number): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), timeoutMs);\n });\n try {\n return await Promise.race([operation, timeout]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\nexport class FactoryTransitionService {\n readonly #configVersion: string;\n readonly #boards: BoardRegistry;\n readonly #storage: WorkItemsStorage;\n readonly #timeoutMs: number;\n readonly #onTerminalStage: FactoryTransitionServiceOptions['onTerminalStage'];\n readonly #terminalCleanupTimeoutMs: number;\n readonly #onAccepted: FactoryTransitionServiceOptions['onAccepted'];\n readonly #onTransitionCommitted: FactoryTransitionServiceOptions['onTransitionCommitted'];\n readonly #autoApprovePlans: FactoryTransitionServiceOptions['autoApprovePlans'];\n readonly #selfReviewEnabled: FactoryTransitionServiceOptions['selfReviewEnabled'];\n readonly #audit: AuditRecorder | undefined;\n\n constructor(options: FactoryTransitionServiceOptions) {\n this.#configVersion = options.configVersion;\n this.#boards = options.boards ?? createBoardRegistry();\n this.#storage = options.storage;\n this.#audit = options.audit;\n this.#timeoutMs = options.timeoutMs ?? RULE_TIMEOUT_MS;\n this.#onTerminalStage = options.onTerminalStage;\n this.#onAccepted = options.onAccepted;\n this.#onTransitionCommitted = options.onTransitionCommitted;\n this.#autoApprovePlans = options.autoApprovePlans;\n this.#selfReviewEnabled = options.selfReviewEnabled;\n this.#terminalCleanupTimeoutMs = options.terminalCleanupTimeoutMs ?? TERMINAL_CLEANUP_TIMEOUT_MS;\n }\n\n get configVersion(): string {\n return this.#configVersion;\n }\n\n async transition(request: FactoryTransitionRequest): Promise<FactoryTransitionResult> {\n const replay = await this.#storage.getTransitionResultByIngress(\n request.orgId,\n request.factoryProjectId,\n request.ingress.identity,\n );\n if (replay) return replay as unknown as FactoryTransitionResult;\n\n const transitionId = request.ingress.transitionId ?? globalThis.crypto.randomUUID();\n const item = await this.#storage.get({ orgId: request.orgId, id: request.workItemId });\n if (!item) {\n const rejection = await this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'Work item not found.',\n );\n await this.#recordTransition(request, undefined, rejection);\n return rejection;\n }\n const result = await this.#evaluateAndCommit(request, transitionId, item);\n await this.#recordTransition(request, item, result);\n this.#notifyCommitted(request, item, result);\n return result;\n }\n\n #notifyCommitted(request: FactoryTransitionRequest, item: WorkItemRow, result: FactoryTransitionResult): void {\n const onTransitionCommitted = this.#onTransitionCommitted;\n if (!onTransitionCommitted || result.status !== 'accepted') return;\n // Board-neutral: `currentStage` only knows the built-in rule stages.\n const fromStage = item.stages.length === 1 ? item.stages[0] : undefined;\n if (result.stage === fromStage) return;\n // Inside the chain so a synchronous throw is isolated the same way an async rejection is.\n void Promise.resolve()\n .then(() =>\n onTransitionCommitted({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: item.id,\n fromStage,\n toStage: result.stage,\n }),\n )\n .catch(error => {\n console.warn(`[factory] stage notification failed for work item ${item.id}:`, error);\n });\n }\n\n /** A rejection can outlive its work item: the row still names the id the caller asked for. */\n async #recordTransition(\n request: FactoryTransitionRequest,\n item: WorkItemRow | undefined,\n result: FactoryTransitionResult,\n ): Promise<void> {\n if (!this.#audit) return;\n const from = item ? currentStage(item.stages) : undefined;\n if (result.status === 'accepted' && result.stage === from && !request.reenter) return;\n const outcome =\n result.status === 'accepted'\n ? { action: 'factory.work_item.stage_moved' as const, to: result.stage, revision: result.revision }\n : {\n action: 'factory.work_item.transition_rejected' as const,\n to: request.stage,\n code: result.code,\n reason: result.reason,\n };\n const { action, ...detail } = outcome;\n await this.#audit\n .record({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n ...auditActorOf(request.actor),\n actorProfile: request.actorProfile,\n ...(request.context ? { context: request.context } : {}),\n action,\n idempotencyKey: result.transitionId,\n targets: [{ type: 'work_item', id: item?.id ?? request.workItemId, ...(item ? { name: item.title } : {}) }],\n metadata: {\n transitionId: result.transitionId,\n ingressType: request.ingress.type,\n cause: request.cause,\n configVersion: this.#configVersion,\n ...(from ? { from } : {}),\n ...(request.reenter ? { reenter: true } : {}),\n ...detail,\n },\n })\n .catch(error => {\n console.warn(`[factory] audit failed for transition ${result.transitionId}:`, error);\n });\n }\n\n async #evaluateAndCommit(\n request: FactoryTransitionRequest,\n transitionId: string,\n item: WorkItemRow,\n ): Promise<FactoryTransitionResult> {\n if (request.causalChain && request.causalChain.length > MAX_FACTORY_RULE_CAUSAL_DEPTH) {\n return this.#commitRejection(\n request,\n transitionId,\n 'causal_depth_exceeded',\n 'Factory rule causal depth exceeded.',\n );\n }\n const itemSource = workItemSource(item.externalSource);\n const source = factoryRuleSourceForWorkItem(itemSource);\n const isPullRequest = source === 'pullRequest' || source === 'gitlabPullRequest';\n const legacyBoard = isPullRequest ? 'review' : 'work';\n if (item.board === null && !this.#boards.has(legacyBoard)) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'This legacy work item has no assigned board. Assign an installed board and phase through the work-item PATCH endpoint before transitioning it.',\n );\n }\n const itemBoard = item.board ?? legacyBoard;\n if (request.board !== itemBoard) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n `The work item belongs to board \"${itemBoard}\", not \"${request.board}\".`,\n );\n }\n if ((itemBoard === 'review' && !isPullRequest) || (itemBoard === 'work' && isPullRequest)) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'The work item does not belong to the requested board.',\n );\n }\n const board = this.#boards.get(request.board);\n if (!board) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n `Board \"${request.board}\" is not installed.`,\n );\n }\n const fromStage = item.stages.length === 1 ? item.stages[0] : undefined;\n if (!fromStage || !Object.prototype.hasOwnProperty.call(board.phases, fromStage)) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'The work item does not have one canonical phase on the requested board.',\n );\n }\n if (\n !Object.prototype.hasOwnProperty.call(board.phases, request.stage) ||\n !board.allowsTransition(fromStage, request.stage)\n ) {\n const nextStages = [...new Set((board.transitions[fromStage] ?? []).map(transition => transition.to))];\n const nextStagesReason =\n nextStages.length > 0\n ? `Next stages declared from ${fromStage}: ${nextStages.join(', ')}.`\n : `No next stage is declared from ${fromStage}.`;\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n `The ${board.title} board does not allow moving from ${fromStage} to ${request.stage}. ${nextStagesReason}`,\n );\n }\n\n // The coordinator's own self-move at run start would otherwise inject a second run's kickoff.\n const humanMove = request.actor.type === 'human' && fromStage !== request.stage && request.cause !== 'run_start';\n // The board, not the phase name, says whether a seat is engaged on either side of this move.\n const entersWorking = board.isWorking(request.stage);\n const seatRole = board.roleForPhase(request.stage);\n\n const contextBase = {\n tenant: { orgId: request.orgId, projectId: request.factoryProjectId },\n actor: request.actor,\n ingress: { type: request.ingress.type, id: request.ingress.identity },\n cause: request.cause,\n causalChain: request.causalChain ?? [],\n configVersion: this.#configVersion,\n item: {\n id: item.id,\n source: itemSource,\n sourceKey: item.externalSource\n ? `${item.externalSource.integrationId}:${item.externalSource.type}:${item.externalSource.externalId}`\n : null,\n parentWorkItemId: item.parentWorkItemId,\n title: item.title,\n url: item.externalSource?.url ?? null,\n stages: [...item.stages],\n acceptedAt: item.acceptedAt,\n metadata: item.metadata,\n },\n board: request.board,\n itemRevision: item.revision,\n source,\n fromStage,\n toStage: request.stage,\n } satisfies Omit<FactoryStageRuleContext, 'stage'>;\n\n let evaluation:\n | { outcome: 'accepted'; decisions: Record<string, unknown>[]; intents: TransitionConsentOptions }\n | { outcome: 'rejected'; code: string; reason: string };\n try {\n evaluation = await withRuleTimeout(\n (async () => {\n // Resolve the project switch inside the timed block so a resolver rejection\n // surfaces as a committed rule_error and a slow lookup is bounded.\n const plansAutoApproved = this.#autoApprovePlans\n ? await this.#autoApprovePlans({ orgId: request.orgId, factoryProjectId: request.factoryProjectId })\n : false;\n const policy = boardTransitionPolicyResultSchema.parse(\n await board.transitionPolicy?.(\n immutablePolicySnapshot({\n ...contextBase,\n item: { ...contextBase.item, triageType: item.triageType },\n initialEntry: request.initialEntry ?? false,\n reenter: request.reenter ?? false,\n isHumanTransition: isHumanTransition(request),\n plansAutoApproved,\n planApproved: request.planApproved === true,\n requestedTriageType: request.triageType,\n }),\n ),\n );\n if (policy?.type === 'reject') {\n return { outcome: 'rejected' as const, code: policy.code, reason: policy.reason };\n }\n if (\n policy?.triageType !== undefined &&\n (!isTriageAgent(request.actor) ||\n policy.triageType !== request.triageType ||\n (item.triageType !== null && item.triageType !== policy.triageType))\n ) {\n throw new Error('Board policy requested an unauthorized classification.');\n }\n if (policy?.accept && !isHumanTransition(request)) {\n throw new Error('Board policy requested unauthorized acceptance.');\n }\n // External content can steer a bound agent; board allowance cannot bypass this guard.\n if (\n request.actor.type === 'agent' &&\n !board.isWorking(fromStage) &&\n entersWorking &&\n externallyAuthoredWorkItem(item)\n ) {\n return {\n outcome: 'rejected' as const,\n code: 'approval_required',\n reason:\n 'This card comes from outside the write-access circle; a person must resume it from the Factory board.',\n };\n }\n const selfReviewEnabled =\n request.board === 'work' && this.#selfReviewEnabled\n ? await this.#selfReviewEnabled({ orgId: request.orgId, factoryProjectId: request.factoryProjectId })\n : false;\n const decisions: FactoryCommitDecision[] = [];\n for (const rule of resolveFactoryStageRules(this.#boards, {\n board: request.board,\n source,\n fromStage,\n toStage: request.stage,\n initialEntry: request.initialEntry,\n reenter: request.reenter,\n })) {\n const context: FactoryStageRuleContext = Object.freeze({\n ...contextBase,\n selfReviewEnabled,\n stage: rule.phase === 'exit' ? fromStage : request.stage,\n });\n const raw = await rule.handler(context);\n if (raw === undefined) continue;\n const decision = validateFactoryRuleDecision(raw, context.causalChain.length);\n if (decision.type === 'reject') {\n return { outcome: 'rejected' as const, code: decision.code, reason: decision.reason };\n }\n assertFactoryDecisionTarget(decision, this.#boards, itemBoard);\n if (startsRun(decision) && runAlreadyUnderway(request, decision)) continue;\n decisions.push(decision);\n }\n const validated = validateFactoryRuleDecisions(decisions);\n if (humanMove) {\n const message = stageTransitionMessage(fromStage, request.stage);\n const skill = validated.find(decision => decision.type === 'invokeSkill');\n if (skill) {\n skill.precedingMessage = message;\n } else {\n validated.unshift({\n type: 'sendMessage',\n idempotencyKey: `factory-stage:${transitionId}`,\n message,\n priority: 'urgent',\n idleBehavior: 'wake',\n // Parking a card says stop: no seat is right by construction, so\n // the notice goes to whichever session is live — or nobody.\n ...(entersWorking && seatRole !== undefined ? { role: seatRole, prepareBinding: true } : {}),\n });\n }\n }\n return {\n outcome: 'accepted' as const,\n intents: { triageType: policy?.triageType, accept: policy?.accept === true && !item.acceptedAt },\n decisions: validateFactoryRuleDecisions(validated) as unknown as Record<string, unknown>[],\n };\n })(),\n this.#timeoutMs,\n );\n } catch (error) {\n const failed =\n error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT'\n ? { code: 'timeout' as const, reason: 'Factory rule evaluation timed out.' }\n : ruleFailure(error);\n evaluation = { outcome: 'rejected', ...failed };\n }\n return this.#commit(\n request,\n transitionId,\n evaluation,\n evaluation.outcome === 'accepted'\n ? { ...consentEffect(request, entersWorking, humanMove), ...evaluation.intents }\n : {},\n );\n }\n\n async #commitRejection(\n request: FactoryTransitionRequest,\n transitionId: string,\n code: FactoryRuleRejectionCode,\n reason: string,\n ): Promise<FactoryTransitionResult> {\n return this.#commit(request, transitionId, {\n outcome: 'rejected',\n code,\n reason: reason.slice(0, MAX_REJECTION_REASON),\n });\n }\n\n async #commit(\n request: FactoryTransitionRequest,\n transitionId: string,\n evaluation:\n | { outcome: 'accepted'; decisions: Record<string, unknown>[] }\n | { outcome: 'rejected'; code: string; reason: string },\n options: TransitionConsentOptions = {},\n ): Promise<FactoryTransitionResult> {\n const committed = await this.#storage.commitTransition({\n autonomy: options.autonomy,\n consentedBy: options.consentedBy,\n ...(options.accept ? { accept: true } : {}),\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: request.workItemId,\n expectedRevision: request.expectedRevision,\n destinationStage: request.stage,\n actorId: actorId(request.actor),\n ingress: { identity: request.ingress.identity, triggerType: request.ingress.type, transitionId },\n configVersion: this.#configVersion,\n causalChain: [...(request.causalChain ?? [])],\n evaluation,\n ...(options.triageType ? { triageType: options.triageType } : {}),\n });\n if (committed.status === 'missing') {\n return rejection(transitionId, request.workItemId, 'invalid_transition', 'Work item not found.');\n }\n const result = committed.result as unknown as FactoryTransitionResult;\n if (\n this.#onAccepted &&\n options.accept &&\n committed.status === 'committed' &&\n result.status === 'accepted' &&\n committed.item?.acceptedAt\n ) {\n const item = committed.item;\n const onAccepted = this.#onAccepted;\n // Invoke inside the chain so a synchronous throw is isolated the same way an async rejection is.\n void Promise.resolve()\n .then(() =>\n onAccepted({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: request.workItemId,\n item,\n }),\n )\n .catch(error => {\n console.warn(`[factory] acceptance hook failed for work item ${request.workItemId}:`, error);\n });\n }\n // Only an installed board's declaration releases resources; an unknown board or phase never does.\n if (\n this.#onTerminalStage &&\n result.status === 'accepted' &&\n this.#boards.get(request.board)?.isTerminal(result.stage) === true\n ) {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n const cleanup = Promise.resolve(\n this.#onTerminalStage({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: request.workItemId,\n stage: result.stage,\n revision: result.revision,\n actor: request.actor,\n }),\n );\n // A late rejection after the timeout wins the race must not surface\n // as an unhandled rejection.\n cleanup.catch(() => {});\n await Promise.race([\n cleanup,\n new Promise<void>(resolve => {\n timer = setTimeout(resolve, this.#terminalCleanupTimeoutMs);\n }),\n ]);\n } catch {\n // Resource release is best-effort — never fail a committed transition.\n } finally {\n clearTimeout(timer);\n }\n }\n return result;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"transition-service.js","names":[],"sources":["../../src/rules/transition-service.ts"],"sourcesContent":["import { createBoardRegistry } from '../boards/index.js';\nimport type { BoardRegistry } from '../boards/index.js';\nimport { boardTransitionPolicyResultSchema, immutablePolicySnapshot } from '../boards/transition-policy.js';\nimport type { AuditActorProfileInput, AuditActorType, AuditContext } from '../storage/domains/audit/base.js';\nimport type { AuditRecorder } from '../storage/domains/audit/domain.js';\nimport { isAgentActor } from '../storage/domains/work-items/base.js';\nimport type { WorkItemRow, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { resolveFactoryStageRules } from './resolve.js';\nimport type {\n FactoryCommitDecision,\n FactoryRuleActor,\n FactoryRuleBoard,\n FactoryRuleCausalEntry,\n FactoryRuleRejectionCode,\n FactoryRuleStage,\n FactoryTriageType,\n FactoryStageRuleContext,\n FactoryTransitionResult,\n} from './types.js';\nimport {\n externallyAuthoredWorkItem,\n factoryRuleSourceForWorkItem,\n isFactoryRuleStage,\n workItemSource,\n} from './types.js';\nimport {\n MAX_FACTORY_RULE_CAUSAL_DEPTH,\n assertFactoryDecisionTarget,\n validateFactoryRuleDecision,\n validateFactoryRuleDecisions,\n} from './validation.js';\n\nconst RULE_TIMEOUT_MS = 5_000;\nconst MAX_REJECTION_REASON = 512;\n/** Longest a committed transition waits for terminal resource cleanup. Cleanup\n * reattaches remote sandboxes, so a hung provider call must not leave the\n * already-committed transition request pending; past this bound the cleanup\n * keeps running in the background as pure best-effort. */\nconst TERMINAL_CLEANUP_TIMEOUT_MS = 30_000;\n\nexport interface FactoryTransitionRequest {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n /** Installed board id; the service rejects boards that are not installed. */\n board: string;\n stage: FactoryRuleStage;\n expectedRevision: number;\n actor: FactoryRuleActor;\n actorProfile?: AuditActorProfileInput;\n /** Where a browser request came from; rules and agents carry none. */\n context?: AuditContext;\n ingress: { type: 'human' | 'agent' | 'toolResult' | 'github' | 'rule'; identity: string; transitionId?: string };\n cause: string;\n causalChain?: readonly FactoryRuleCausalEntry[];\n /** Internal materialization path: evaluate only the destination onEnter leaf even when already at that stage. */\n initialEntry?: boolean;\n /** Re-runs the stage's entry rules when the item already holds that stage, to restart work the entry invalidated. */\n reenter?: boolean;\n /** Structured verdict required from a bound triage-agent terminal request. */\n triageType?: FactoryTriageType;\n /** Internal proof that this move consumes an approved `submit_plan` result. */\n planApproved?: true;\n}\n\nexport interface FactoryTransitionServiceOptions {\n configVersion: string;\n storage: WorkItemsStorage;\n boards?: BoardRegistry;\n /** Every commit, accepted or rejected, lands here as `stage_moved` / `transition_rejected` under the request's actor. */\n audit?: AuditRecorder;\n timeoutMs?: number;\n /**\n * Called after a transition commits into a phase the board declares\n * terminal — the point where the item's sessions stop receiving runs, so\n * resources they hold (e.g. sandboxes) can be released for reuse. Awaited,\n * but failures are swallowed: releasing resources must never break or roll\n * back the committed transition.\n */\n onTerminalStage?: (args: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n stage: FactoryRuleStage;\n revision: number;\n /** The actor that committed this terminal transition. Lets cleanup leave\n * the seat that drove its own transition (an agent tool call) untouched. */\n actor: FactoryRuleActor;\n }) => Promise<void> | void;\n /**\n * Called after a transition commits a stage change, e.g. to tell the item's\n * followers. Not awaited, and failures are logged: a notification must never\n * fail or delay the committed transition.\n */\n onTransitionCommitted?: (args: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n fromStage: string | undefined;\n toStage: string;\n }) => Promise<void> | void;\n /** Upper bound on how long a committed transition waits for\n * `onTerminalStage` before returning (default 30s). The cleanup continues\n * in the background past the bound. */\n terminalCleanupTimeoutMs?: number;\n /**\n * Called after a transition first records a person's acceptance of a\n * non-bug item (see `WorkItemRow.acceptedAt`). Fire-and-forget: failures\n * are swallowed, the committed transition never depends on it.\n */\n onAccepted?: (args: {\n orgId: string;\n factoryProjectId: string;\n workItemId: string;\n item: WorkItemRow;\n }) => Promise<void> | void;\n /**\n * Resolves whether a project auto-approves produced plans. Unset means off:\n * a plan without explicit approval is a plan a person must review.\n */\n autoApprovePlans?: (tenant: { orgId: string; factoryProjectId: string }) => Promise<boolean>;\n /** Resolves whether a project self-reviews Work items before the change request opens. Unset means off. */\n selfReviewEnabled?: (tenant: { orgId: string; factoryProjectId: string }) => Promise<boolean>;\n}\n\nfunction rejection(\n transitionId: string,\n itemId: string,\n code: FactoryRuleRejectionCode,\n reason: string,\n): FactoryTransitionResult {\n return { status: 'rejected', transitionId, itemId, code, reason: reason.slice(0, MAX_REJECTION_REASON) };\n}\n\nfunction actorId(actor: FactoryRuleActor): string {\n switch (actor.type) {\n case 'human':\n case 'system':\n return actor.id;\n case 'agent':\n return `agent:${actor.bindingId}`;\n case 'github':\n return `github:${actor.login}`;\n case 'gitlab':\n return `gitlab:${actor.username}`;\n }\n}\n\n// The dispatcher executes an agent-approved decision as a human actor so its consent carries; the trail still names the agent.\nexport function auditActorOf(actor: FactoryRuleActor): { actorId: string; actorType: AuditActorType } {\n const id = actorId(actor);\n switch (actor.type) {\n case 'github':\n case 'gitlab':\n return { actorId: id, actorType: 'human' };\n case 'human':\n return { actorId: id, actorType: isAgentActor(id) ? 'agent' : 'human' };\n default:\n return { actorId: id, actorType: actor.type };\n }\n}\n\nexport function currentStage(stages: readonly string[]): FactoryRuleStage | undefined {\n if (stages.length !== 1) return undefined;\n const stage = stages[0];\n return isFactoryRuleStage(stage) ? stage : undefined;\n}\n\ninterface TransitionConsentOptions {\n autonomy?: 'arm' | 'disarm';\n consentedBy?: string;\n accept?: boolean;\n triageType?: FactoryTriageType;\n}\n\n// Entering a resting lane disarms whoever rests it; only a person's move into a working lane arms.\nfunction transitionConsent(working: boolean, humanMove: boolean): 'arm' | 'disarm' | undefined {\n if (!working) return 'disarm';\n return humanMove ? 'arm' : undefined;\n}\n\n// An event arriving as data (GitHub, sweeps) never pre-approves the runs its transition queues.\nfunction bearsConsent(actor: FactoryRuleActor): boolean {\n return actor.type === 'human' || actor.type === 'agent';\n}\n\n// Rides the transition's own revision-checked commit, so a stale or rejected commit flips nothing.\nfunction consentEffect(\n request: FactoryTransitionRequest,\n working: boolean,\n humanMove: boolean,\n): TransitionConsentOptions {\n const autonomy = transitionConsent(working, humanMove);\n return bearsConsent(request.actor) ? { autonomy, consentedBy: actorId(request.actor) } : { autonomy };\n}\n\ntype RunStartDecision = Extract<FactoryCommitDecision, { type: 'invokeSkill' | 'sendMessage' }>;\n\nfunction startsRun(decision: FactoryCommitDecision): decision is RunStartDecision {\n return decision.type === 'invokeSkill' || (decision.type === 'sendMessage' && decision.prepareBinding === true);\n}\n\n// Answering a recorded run start, or the role's own mid-run agent, with a run would start a second one.\nfunction runAlreadyUnderway(request: FactoryTransitionRequest, decision: RunStartDecision): boolean {\n if (request.cause === 'run_start') return true;\n return request.actor.type === 'agent' && request.actor.role === decision.role;\n}\n\nfunction stageTransitionMessage(fromStage: FactoryRuleStage, toStage: FactoryRuleStage): string {\n return `This work was moved from the ${fromStage} stage to the ${toStage} stage.`;\n}\n\nfunction isTriageAgent(actor: FactoryRuleActor): actor is Extract<FactoryRuleActor, { type: 'agent' }> {\n return actor.type === 'agent' && actor.role === 'triage';\n}\n\nfunction isHumanTransition(request: FactoryTransitionRequest): boolean {\n return request.actor.type === 'human' && request.ingress.type === 'human';\n}\n\nfunction ruleFailure(error: unknown): { code: FactoryRuleRejectionCode; reason: string } {\n return {\n code: 'rule_error',\n reason: error instanceof Error ? `Factory rule failed: ${error.message}` : 'Factory rule failed.',\n };\n}\n\nasync function withRuleTimeout<T>(operation: Promise<T>, timeoutMs: number): Promise<T> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error('FACTORY_RULE_TIMEOUT')), timeoutMs);\n });\n try {\n return await Promise.race([operation, timeout]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n}\n\nexport class FactoryTransitionService {\n readonly #configVersion: string;\n readonly #boards: BoardRegistry;\n readonly #storage: WorkItemsStorage;\n readonly #timeoutMs: number;\n readonly #onTerminalStage: FactoryTransitionServiceOptions['onTerminalStage'];\n readonly #terminalCleanupTimeoutMs: number;\n readonly #onAccepted: FactoryTransitionServiceOptions['onAccepted'];\n readonly #onTransitionCommitted: FactoryTransitionServiceOptions['onTransitionCommitted'];\n readonly #autoApprovePlans: FactoryTransitionServiceOptions['autoApprovePlans'];\n readonly #selfReviewEnabled: FactoryTransitionServiceOptions['selfReviewEnabled'];\n readonly #audit: AuditRecorder | undefined;\n\n constructor(options: FactoryTransitionServiceOptions) {\n this.#configVersion = options.configVersion;\n this.#boards = options.boards ?? createBoardRegistry();\n this.#storage = options.storage;\n this.#audit = options.audit;\n this.#timeoutMs = options.timeoutMs ?? RULE_TIMEOUT_MS;\n this.#onTerminalStage = options.onTerminalStage;\n this.#onAccepted = options.onAccepted;\n this.#onTransitionCommitted = options.onTransitionCommitted;\n this.#autoApprovePlans = options.autoApprovePlans;\n this.#selfReviewEnabled = options.selfReviewEnabled;\n this.#terminalCleanupTimeoutMs = options.terminalCleanupTimeoutMs ?? TERMINAL_CLEANUP_TIMEOUT_MS;\n }\n\n get configVersion(): string {\n return this.#configVersion;\n }\n\n async transition(request: FactoryTransitionRequest): Promise<FactoryTransitionResult> {\n const replay = await this.#storage.getTransitionResultByIngress(\n request.orgId,\n request.factoryProjectId,\n request.ingress.identity,\n );\n if (replay) return replay as unknown as FactoryTransitionResult;\n\n const transitionId = request.ingress.transitionId ?? globalThis.crypto.randomUUID();\n const item = await this.#storage.get({ orgId: request.orgId, id: request.workItemId });\n if (!item) {\n const rejection = await this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'Work item not found.',\n );\n await this.#recordTransition(request, undefined, rejection);\n return rejection;\n }\n const result = await this.#evaluateAndCommit(request, transitionId, item);\n await this.#recordTransition(request, item, result);\n this.#notifyCommitted(request, item, result);\n return result;\n }\n\n #notifyCommitted(request: FactoryTransitionRequest, item: WorkItemRow, result: FactoryTransitionResult): void {\n const onTransitionCommitted = this.#onTransitionCommitted;\n if (!onTransitionCommitted || result.status !== 'accepted') return;\n // Board-neutral: `currentStage` only knows the built-in rule stages.\n const fromStage = item.stages.length === 1 ? item.stages[0] : undefined;\n if (result.stage === fromStage) return;\n // Inside the chain so a synchronous throw is isolated the same way an async rejection is.\n void Promise.resolve()\n .then(() =>\n onTransitionCommitted({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: item.id,\n fromStage,\n toStage: result.stage,\n }),\n )\n .catch(error => {\n console.warn(`[factory] stage notification failed for work item ${item.id}:`, error);\n });\n }\n\n /** A rejection can outlive its work item: the row still names the id the caller asked for. */\n async #recordTransition(\n request: FactoryTransitionRequest,\n item: WorkItemRow | undefined,\n result: FactoryTransitionResult,\n ): Promise<void> {\n if (!this.#audit) return;\n const from = item ? currentStage(item.stages) : undefined;\n if (result.status === 'accepted' && result.stage === from && !request.reenter) return;\n const outcome =\n result.status === 'accepted'\n ? { action: 'factory.work_item.stage_moved' as const, to: result.stage, revision: result.revision }\n : {\n action: 'factory.work_item.transition_rejected' as const,\n to: request.stage,\n code: result.code,\n reason: result.reason,\n };\n const { action, ...detail } = outcome;\n await this.#audit\n .record({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n ...auditActorOf(request.actor),\n actorProfile: request.actorProfile,\n ...(request.context ? { context: request.context } : {}),\n action,\n idempotencyKey: result.transitionId,\n targets: [{ type: 'work_item', id: item?.id ?? request.workItemId, ...(item ? { name: item.title } : {}) }],\n metadata: {\n transitionId: result.transitionId,\n ingressType: request.ingress.type,\n cause: request.cause,\n configVersion: this.#configVersion,\n ...(from ? { from } : {}),\n ...(request.reenter ? { reenter: true } : {}),\n ...detail,\n },\n })\n .catch(error => {\n console.warn(`[factory] audit failed for transition ${result.transitionId}:`, error);\n });\n }\n\n async #evaluateAndCommit(\n request: FactoryTransitionRequest,\n transitionId: string,\n item: WorkItemRow,\n ): Promise<FactoryTransitionResult> {\n if (request.causalChain && request.causalChain.length > MAX_FACTORY_RULE_CAUSAL_DEPTH) {\n return this.#commitRejection(\n request,\n transitionId,\n 'causal_depth_exceeded',\n 'Factory rule causal depth exceeded.',\n );\n }\n const itemSource = workItemSource(item.externalSource);\n const source = factoryRuleSourceForWorkItem(itemSource);\n const isPullRequest = source === 'pullRequest' || source === 'gitlabPullRequest';\n const legacyBoard = isPullRequest ? 'review' : 'work';\n if (item.board === null && !this.#boards.has(legacyBoard)) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'This legacy work item has no assigned board. Assign an installed board and phase through the work-item PATCH endpoint before transitioning it.',\n );\n }\n const itemBoard = item.board ?? legacyBoard;\n if (request.board !== itemBoard) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n `The work item belongs to board \"${itemBoard}\", not \"${request.board}\".`,\n );\n }\n if ((itemBoard === 'review' && !isPullRequest) || (itemBoard === 'work' && isPullRequest)) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'The work item does not belong to the requested board.',\n );\n }\n const board = this.#boards.get(request.board);\n if (!board) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n `Board \"${request.board}\" is not installed.`,\n );\n }\n const fromStage = item.stages.length === 1 ? item.stages[0] : undefined;\n if (!fromStage || !Object.prototype.hasOwnProperty.call(board.phases, fromStage)) {\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n 'The work item does not have one canonical phase on the requested board.',\n );\n }\n if (\n !Object.prototype.hasOwnProperty.call(board.phases, request.stage) ||\n !board.allowsTransition(fromStage, request.stage)\n ) {\n const nextStages = [...new Set((board.transitions[fromStage] ?? []).map(transition => transition.to))];\n const nextStagesReason =\n nextStages.length > 0\n ? `Next stages declared from ${fromStage}: ${nextStages.join(', ')}.`\n : `No next stage is declared from ${fromStage}.`;\n return this.#commitRejection(\n request,\n transitionId,\n 'invalid_transition',\n `The ${board.title} board does not allow moving from ${fromStage} to ${request.stage}. ${nextStagesReason}`,\n );\n }\n\n // The coordinator's own self-move at run start would otherwise inject a second run's kickoff.\n const humanMove = request.actor.type === 'human' && fromStage !== request.stage && request.cause !== 'run_start';\n // The board, not the phase name, says whether a seat is engaged on either side of this move.\n const entersWorking = board.isWorking(request.stage);\n const seatRole = board.roleForPhase(request.stage);\n\n const contextBase = {\n tenant: { orgId: request.orgId, projectId: request.factoryProjectId },\n actor: request.actor,\n ingress: { type: request.ingress.type, id: request.ingress.identity },\n cause: request.cause,\n causalChain: request.causalChain ?? [],\n configVersion: this.#configVersion,\n item: {\n id: item.id,\n source: itemSource,\n sourceKey: item.externalSource\n ? `${item.externalSource.integrationId}:${item.externalSource.type}:${item.externalSource.externalId}`\n : null,\n parentWorkItemId: item.parentWorkItemId,\n title: item.title,\n url: item.externalSource?.url ?? null,\n stages: [...item.stages],\n acceptedAt: item.acceptedAt,\n metadata: item.metadata,\n },\n board: request.board,\n itemRevision: item.revision,\n source,\n fromStage,\n toStage: request.stage,\n } satisfies Omit<FactoryStageRuleContext, 'stage'>;\n\n let evaluation:\n | { outcome: 'accepted'; decisions: Record<string, unknown>[]; intents: TransitionConsentOptions }\n | { outcome: 'rejected'; code: string; reason: string };\n try {\n evaluation = await withRuleTimeout(\n (async () => {\n // Resolve the project switch inside the timed block so a resolver rejection\n // surfaces as a committed rule_error and a slow lookup is bounded.\n const plansAutoApproved = this.#autoApprovePlans\n ? await this.#autoApprovePlans({ orgId: request.orgId, factoryProjectId: request.factoryProjectId })\n : false;\n const policy = boardTransitionPolicyResultSchema.parse(\n await board.transitionPolicy?.(\n immutablePolicySnapshot({\n ...contextBase,\n item: { ...contextBase.item, triageType: item.triageType },\n initialEntry: request.initialEntry ?? false,\n reenter: request.reenter ?? false,\n isHumanTransition: isHumanTransition(request),\n plansAutoApproved,\n planApproved: request.planApproved === true,\n requestedTriageType: request.triageType,\n }),\n ),\n );\n if (policy?.type === 'reject') {\n return { outcome: 'rejected' as const, code: policy.code, reason: policy.reason };\n }\n if (\n policy?.triageType !== undefined &&\n (!isTriageAgent(request.actor) ||\n policy.triageType !== request.triageType ||\n (item.triageType !== null && item.triageType !== policy.triageType))\n ) {\n throw new Error('Board policy requested an unauthorized classification.');\n }\n if (policy?.accept && !isHumanTransition(request)) {\n throw new Error('Board policy requested unauthorized acceptance.');\n }\n // External content can steer a bound agent; board allowance cannot bypass this guard.\n if (\n request.actor.type === 'agent' &&\n !board.isWorking(fromStage) &&\n entersWorking &&\n externallyAuthoredWorkItem(item)\n ) {\n return {\n outcome: 'rejected' as const,\n code: 'approval_required',\n reason:\n 'This card comes from outside the write-access circle; a person must resume it from the Factory board.',\n };\n }\n const selfReviewEnabled =\n request.board === 'work' && this.#selfReviewEnabled\n ? await this.#selfReviewEnabled({ orgId: request.orgId, factoryProjectId: request.factoryProjectId })\n : false;\n const decisions: FactoryCommitDecision[] = [];\n for (const rule of resolveFactoryStageRules(this.#boards, {\n board: request.board,\n source,\n fromStage,\n toStage: request.stage,\n initialEntry: request.initialEntry,\n reenter: request.reenter,\n })) {\n const context: FactoryStageRuleContext = Object.freeze({\n ...contextBase,\n selfReviewEnabled,\n stage: rule.phase === 'exit' ? fromStage : request.stage,\n });\n const raw = await rule.handler(context);\n if (raw === undefined) continue;\n const decision = validateFactoryRuleDecision(raw, context.causalChain.length);\n if (decision.type === 'reject') {\n return { outcome: 'rejected' as const, code: decision.code, reason: decision.reason };\n }\n assertFactoryDecisionTarget(decision, this.#boards, itemBoard);\n if (startsRun(decision) && runAlreadyUnderway(request, decision)) continue;\n decisions.push(decision);\n }\n const validated = validateFactoryRuleDecisions(decisions);\n if (humanMove) {\n const message = stageTransitionMessage(fromStage, request.stage);\n const skill = validated.find(decision => decision.type === 'invokeSkill');\n if (skill) {\n skill.precedingMessage = message;\n } else {\n validated.unshift({\n type: 'sendMessage',\n idempotencyKey: `factory-stage:${transitionId}`,\n message,\n priority: 'urgent',\n idleBehavior: 'wake',\n // Parking a card says stop: no seat is right by construction, so\n // the notice goes to whichever session is live — or nobody.\n ...(entersWorking && seatRole !== undefined ? { role: seatRole, prepareBinding: true } : {}),\n });\n }\n }\n return {\n outcome: 'accepted' as const,\n intents: { triageType: policy?.triageType, accept: policy?.accept === true && !item.acceptedAt },\n decisions: validateFactoryRuleDecisions(validated) as unknown as Record<string, unknown>[],\n };\n })(),\n this.#timeoutMs,\n );\n } catch (error) {\n const failed =\n error instanceof Error && error.message === 'FACTORY_RULE_TIMEOUT'\n ? { code: 'timeout' as const, reason: 'Factory rule evaluation timed out.' }\n : ruleFailure(error);\n evaluation = { outcome: 'rejected', ...failed };\n }\n return this.#commit(\n request,\n transitionId,\n evaluation,\n evaluation.outcome === 'accepted'\n ? { ...consentEffect(request, entersWorking, humanMove), ...evaluation.intents }\n : {},\n );\n }\n\n async #commitRejection(\n request: FactoryTransitionRequest,\n transitionId: string,\n code: FactoryRuleRejectionCode,\n reason: string,\n ): Promise<FactoryTransitionResult> {\n return this.#commit(request, transitionId, {\n outcome: 'rejected',\n code,\n reason: reason.slice(0, MAX_REJECTION_REASON),\n });\n }\n\n async #commit(\n request: FactoryTransitionRequest,\n transitionId: string,\n evaluation:\n | { outcome: 'accepted'; decisions: Record<string, unknown>[] }\n | { outcome: 'rejected'; code: string; reason: string },\n options: TransitionConsentOptions = {},\n ): Promise<FactoryTransitionResult> {\n const committed = await this.#storage.commitTransition({\n autonomy: options.autonomy,\n consentedBy: options.consentedBy,\n ...(options.accept ? { accept: true } : {}),\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: request.workItemId,\n expectedRevision: request.expectedRevision,\n destinationStage: request.stage,\n actorId: actorId(request.actor),\n ingress: { identity: request.ingress.identity, triggerType: request.ingress.type, transitionId },\n configVersion: this.#configVersion,\n causalChain: [...(request.causalChain ?? [])],\n evaluation,\n ...(options.triageType ? { triageType: options.triageType } : {}),\n });\n if (committed.status === 'missing') {\n return rejection(transitionId, request.workItemId, 'invalid_transition', 'Work item not found.');\n }\n const result = committed.result as unknown as FactoryTransitionResult;\n if (\n this.#onAccepted &&\n options.accept &&\n committed.status === 'committed' &&\n result.status === 'accepted' &&\n committed.item?.acceptedAt\n ) {\n const item = committed.item;\n const onAccepted = this.#onAccepted;\n // Invoke inside the chain so a synchronous throw is isolated the same way an async rejection is.\n void Promise.resolve()\n .then(() =>\n onAccepted({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: request.workItemId,\n item,\n }),\n )\n .catch(error => {\n console.warn(`[factory] acceptance hook failed for work item ${request.workItemId}:`, error);\n });\n }\n // Only an installed board's declaration releases resources; an unknown board or phase never does.\n if (\n this.#onTerminalStage &&\n result.status === 'accepted' &&\n this.#boards.get(request.board)?.isTerminal(result.stage) === true\n ) {\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n const cleanup = Promise.resolve(\n this.#onTerminalStage({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: request.workItemId,\n stage: result.stage,\n revision: result.revision,\n actor: request.actor,\n }),\n );\n // A late rejection after the timeout wins the race must not surface\n // as an unhandled rejection.\n cleanup.catch(() => {});\n await Promise.race([\n cleanup,\n new Promise<void>(resolve => {\n timer = setTimeout(resolve, this.#terminalCleanupTimeoutMs);\n }),\n ]);\n } catch {\n // Resource release is best-effort — never fail a committed transition.\n } finally {\n clearTimeout(timer);\n }\n }\n return result;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"callback-factory-sandbox.js","names":[
|
|
1
|
+
{"version":3,"file":"callback-factory-sandbox.js","names":[],"sources":["../../src/sandbox/callback-factory-sandbox.ts"],"sourcesContent":["import { FactorySandbox } from '@mastra/core/workspace';\nimport type { FactorySandboxContext, MastraSandbox } from '@mastra/core/workspace';\n\nimport type { MastraFactorySandboxConfig } from './session-sandbox.js';\n\n/**\n * Wraps the `sandbox: ctx => new SomeSandbox(...)` host callback\n * as a FactorySandbox. The host owns everything inside the callback, so the\n * factory knows nothing about it: no settings, no template, no builds.\n */\nexport class CallbackFactorySandbox extends FactorySandbox<Record<string, never>> {\n readonly provider = 'custom';\n\n readonly #callback: MastraFactorySandboxConfig;\n\n constructor(callback: MastraFactorySandboxConfig) {\n super();\n this.#callback = callback;\n }\n\n create(ctx: FactorySandboxContext): MastraSandbox {\n return this.#callback(ctx);\n }\n}\n"],"mappings":";;;;;;;AAUA,IAAa,yBAAb,cAA4C,eAAsC;CAChF,WAAoB;CAEpB;CAEA,YAAY,UAAsC;EAChD,MAAM;EACN,KAAK,YAAY;CACnB;CAEA,OAAO,KAA2C;EAChD,OAAO,KAAK,UAAU,GAAG;CAC3B;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-factory-sandbox.js","names":[
|
|
1
|
+
{"version":3,"file":"local-factory-sandbox.js","names":[],"sources":["../../src/sandbox/local-factory-sandbox.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { FactorySandbox, LocalSandbox } from '@mastra/core/workspace';\nimport type { FactorySandboxContext } from '@mastra/core/workspace';\n\nexport interface LocalFactorySandboxOptions {\n /** Directory under which every session gets its own working directory. */\n root: string;\n /** Environment for commands run in the sandbox. */\n env?: Record<string, string>;\n}\n\n/**\n * Sessions run on the host machine: each session's sandbox is a LocalSandbox\n * rooted at `join(root, sessionId)`, where the repository checks out as a\n * subdirectory. No template and no user-tunable settings.\n */\nexport class LocalFactorySandbox extends FactorySandbox<Record<string, never>> {\n readonly provider = 'local';\n\n readonly #options: LocalFactorySandboxOptions;\n\n constructor(options: LocalFactorySandboxOptions) {\n super();\n this.#options = options;\n }\n\n create(ctx: FactorySandboxContext): LocalSandbox {\n return new LocalSandbox({\n id: ctx.sessionId,\n timeout: 30_000,\n workingDirectory: join(this.#options.root, ctx.sessionId),\n ...(this.#options.env ? { env: this.#options.env } : {}),\n });\n }\n}\n"],"mappings":";;;;;;;;AAgBA,IAAa,sBAAb,cAAyC,eAAsC;CAC7E,WAAoB;CAEpB;CAEA,YAAY,SAAqC;EAC/C,MAAM;EACN,KAAK,WAAW;CAClB;CAEA,OAAO,KAA0C;EAC/C,OAAO,IAAI,aAAa;GACtB,IAAI,IAAI;GACR,SAAS;GACT,kBAAkB,KAAK,KAAK,SAAS,MAAM,IAAI,SAAS;GACxD,GAAI,KAAK,SAAS,MAAM,EAAE,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC;EACxD,CAAC;CACH;AACF"}
|
|
@@ -52,6 +52,17 @@ export declare class SessionRetirementCoordinator {
|
|
|
52
52
|
* A destroy failure keeps the persisted id so a retry can reach the VM.
|
|
53
53
|
*/
|
|
54
54
|
recreateSessionSandbox(input: RecreateSessionSandboxInput): Promise<'recreated' | 'not-found'>;
|
|
55
|
+
/**
|
|
56
|
+
* Replace the session's VM while keeping its sandbox instance, for the
|
|
57
|
+
* session's own agent. A run holds the instance it started with until the
|
|
58
|
+
* run ends, so recreate (which destroys the instance) would leave every
|
|
59
|
+
* later command in that run failing. Here the VM is destroyed and the
|
|
60
|
+
* instance is left `stopped` with no VM id, so the run's next command starts
|
|
61
|
+
* a fresh VM and the start hook sets it up again.
|
|
62
|
+
*
|
|
63
|
+
* `not-here` means this process holds no instance for the session.
|
|
64
|
+
*/
|
|
65
|
+
resetSessionSandbox(input: RecreateSessionSandboxInput): Promise<'reset' | 'not-found' | 'not-here'>;
|
|
55
66
|
retireWorkItemSessions(options: {
|
|
56
67
|
workItems: Pick<WorkItemsStorage, 'get'>;
|
|
57
68
|
sourceControl: SourceControlStorageHandle;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-retirement.d.ts","sourceRoot":"","sources":["../../src/sandbox/session-retirement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAoB,MAAM,wBAAwB,CAAC;AAK/E,OAAO,KAAK,EAGV,0BAA0B,EAC3B,MAAM,2CAA2C,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAI9E,KAAK,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;AAEjF,MAAM,WAAW,mCAAmC;IAClD,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAChE;;;;OAIG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,2BAA2B;IAC1C,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAgBD,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,0BAA0B,CAAC;IAC1C,8EAA8E;IAC9E,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,wBAAwB,CAAC,CAAC;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;CACxB;AAQD;;;;;;;;;;;;GAYG;AACH,qBAAa,4BAA4B;;gBAO3B,OAAO,GAAE,mCAAwC;IAOvD,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7D;;;;;;OAMG;IACG,sBAAsB,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,CAAC,WAAW,GAAG,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"session-retirement.d.ts","sourceRoot":"","sources":["../../src/sandbox/session-retirement.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAoB,MAAM,wBAAwB,CAAC;AAK/E,OAAO,KAAK,EAGV,0BAA0B,EAC3B,MAAM,2CAA2C,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAI9E,KAAK,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;AAEjF,MAAM,WAAW,mCAAmC;IAClD,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAChE;;;;OAIG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,2BAA2B;IAC1C,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAgBD,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,0BAA0B,CAAC;IAC1C,8EAA8E;IAC9E,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,wBAAwB,CAAC,CAAC;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,OAAO,CAAC;CACxB;AAQD;;;;;;;;;;;;GAYG;AACH,qBAAa,4BAA4B;;gBAO3B,OAAO,GAAE,mCAAwC;IAOvD,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7D;;;;;;OAMG;IACG,sBAAsB,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,CAAC,WAAW,GAAG,WAAW,CAAC;IAIpG;;;;;;;;;OASG;IACG,mBAAmB,CAAC,KAAK,EAAE,2BAA2B,GAAG,OAAO,CAAC,OAAO,GAAG,WAAW,GAAG,UAAU,CAAC;IAepG,sBAAsB,CAAC,OAAO,EAAE;QACpC,SAAS,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;QACzC,aAAa,EAAE,0BAA0B,CAAC;QAC1C,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBX,+BAA+B,CAAC,OAAO,EAAE;QAC7C,aAAa,EAAE,0BAA0B,CAAC;QAC1C,SAAS,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,wBAAwB,CAAC,CAAC;QAC7D,KAAK,EAAE,MAAM,CAAC;QACd,mBAAmB,EAAE,MAAM,CAAC;KAC7B,GAAG,OAAO,CAAC,IAAI,CAAC;CA6RlB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DEFAULT_COMMAND_TIMEOUT_MS, runTeardownCommand } from "../integrations/github/sandbox.js";
|
|
2
2
|
import { requireExec } from "./materialization.js";
|
|
3
|
-
import { evictSessionSandbox, peekSessionSandbox } from "./session-sandbox.js";
|
|
3
|
+
import { clearFailedSetupCommands, evictSessionSandbox, peekSessionSandbox } from "./session-sandbox.js";
|
|
4
4
|
import { peekSessionEnvironmentTeardown } from "../session/environment-state-processor.js";
|
|
5
5
|
import { destroyDetachedSessionSandbox, releaseSessionSandbox } from "../integrations/github/sandbox-release.js";
|
|
6
6
|
//#region src/sandbox/session-retirement.ts
|
|
@@ -59,6 +59,19 @@ var SessionRetirementCoordinator = class {
|
|
|
59
59
|
async recreateSessionSandbox(input) {
|
|
60
60
|
return this.#withSessionLock(input.sessionId, () => this.#recreateSessionSandbox(input));
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Replace the session's VM while keeping its sandbox instance, for the
|
|
64
|
+
* session's own agent. A run holds the instance it started with until the
|
|
65
|
+
* run ends, so recreate (which destroys the instance) would leave every
|
|
66
|
+
* later command in that run failing. Here the VM is destroyed and the
|
|
67
|
+
* instance is left `stopped` with no VM id, so the run's next command starts
|
|
68
|
+
* a fresh VM and the start hook sets it up again.
|
|
69
|
+
*
|
|
70
|
+
* `not-here` means this process holds no instance for the session.
|
|
71
|
+
*/
|
|
72
|
+
async resetSessionSandbox(input) {
|
|
73
|
+
return this.#withSessionLock(input.sessionId, () => this.#resetSessionSandbox(input));
|
|
74
|
+
}
|
|
62
75
|
async #withSessionLock(sessionId, run) {
|
|
63
76
|
const current = (this.#locks.get(sessionId) ?? Promise.resolve()).catch(() => {}).then(run);
|
|
64
77
|
this.#locks.set(sessionId, current);
|
|
@@ -206,6 +219,30 @@ var SessionRetirementCoordinator = class {
|
|
|
206
219
|
await this.#invalidate(session);
|
|
207
220
|
return "recreated";
|
|
208
221
|
}
|
|
222
|
+
async #resetSessionSandbox(input) {
|
|
223
|
+
const session = await input.sourceControl.sessions.getBySessionId(input.sessionId);
|
|
224
|
+
if (!session || session.orgId !== input.orgId) return "not-found";
|
|
225
|
+
const sandbox = peekSessionSandbox(session.id)?.sandbox;
|
|
226
|
+
if (!sandbox) return "not-here";
|
|
227
|
+
const destroy = async () => {
|
|
228
|
+
if (!await this.#settledWithin(this.#destroyDirectly(sandbox))) throw new Error("Timed out destroying the sandbox. Try again.");
|
|
229
|
+
};
|
|
230
|
+
await destroy();
|
|
231
|
+
const starting = sandbox._startPromise;
|
|
232
|
+
if (starting) {
|
|
233
|
+
if (!await this.#settledWithin(starting.then(() => {}, () => {}))) throw new Error("The old sandbox is still starting. Try again.");
|
|
234
|
+
await destroy();
|
|
235
|
+
}
|
|
236
|
+
sandbox.status = "stopped";
|
|
237
|
+
clearFailedSetupCommands(session.id);
|
|
238
|
+
const latest = await input.sourceControl.sessions.getBySessionId(input.sessionId);
|
|
239
|
+
await input.sourceControl.sessions.setSandbox({
|
|
240
|
+
id: session.id,
|
|
241
|
+
sandboxId: null,
|
|
242
|
+
sandboxWorkdir: latest?.sandboxWorkdir ?? session.sandboxWorkdir ?? ""
|
|
243
|
+
});
|
|
244
|
+
return "reset";
|
|
245
|
+
}
|
|
209
246
|
/**
|
|
210
247
|
* Destroy the session's VM. `_destroy()` silently no-ops on a never-started
|
|
211
248
|
* (`pending`) instance, so pending and detached instances call the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-retirement.js","names":["#invalidateSession","#sandbox","#warn","#destroyTimeoutMs","#locks","#withSessionLock","#retireSession","#recreateSessionSandbox","#teardownSessionSandbox","#invalidate","#destroyDetachedSandbox","#releaseSandbox","#destroyForRecreate","#settledWithin","#destroyDetachedForRecreate","#destroyDirectly"],"sources":["../../src/sandbox/session-retirement.ts"],"sourcesContent":["import type { FactorySandbox, WorkspaceSandbox } from '@mastra/core/workspace';\n\nimport { destroyDetachedSessionSandbox, releaseSessionSandbox } from '../integrations/github/sandbox-release.js';\nimport { DEFAULT_COMMAND_TIMEOUT_MS, runTeardownCommand } from '../integrations/github/sandbox.js';\nimport { peekSessionEnvironmentTeardown } from '../session/environment-state-processor.js';\nimport type {\n ProjectRepository,\n SourceControlSession,\n SourceControlStorageHandle,\n} from '../storage/domains/source-control/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { requireExec } from './materialization.js';\nimport { evictSessionSandbox, peekSessionSandbox } from './session-sandbox.js';\n\ntype WarningLogger = (message: string, details: Record<string, unknown>) => void;\n\nexport interface SessionRetirementCoordinatorOptions {\n invalidateSession?: (sessionId: string) => Promise<void> | void;\n /**\n * The deploy's sandbox configuration. Used to destroy a deleted session's\n * VM by identity when this process never attached to it (after a restart,\n * or on another replica). Recreate uses it the same way.\n */\n sandbox?: FactorySandbox;\n warn?: WarningLogger;\n /** How long recreate waits on a started sandbox's lifecycle destroy before falling back. */\n destroyTimeoutMs?: number;\n}\n\nexport interface RecreateSessionSandboxInput {\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n sessionId: string;\n}\n\nconst DEFAULT_DESTROY_TIMEOUT_MS = 30_000;\n\n/**\n * A provider error meaning the VM is already gone: 410, or a 404 the proxy\n * marks `not_found` (or leaves uncoded). A routing 404 with another code is\n * not proof the VM is gone.\n */\nfunction isSandboxGoneError(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) return false;\n const { status, code } = error as { status?: unknown; code?: unknown };\n if (status === 410) return true;\n return status === 404 && (code === undefined || code === null || code === 'not_found');\n}\n\nexport interface RetireSessionInput {\n sourceControl: SourceControlStorageHandle;\n /** When provided, deleting the session row also strips its work-item refs. */\n workItems?: Pick<WorkItemsStorage, 'clearSessionReferences'>;\n orgId: string;\n sessionId: string;\n deleteSession: boolean;\n}\n\nfunction boundedError(error: unknown): string {\n const detail = error instanceof Error ? error.message : String(error);\n if (detail.length <= 2000) return detail;\n return `${detail.slice(0, 200)}...${detail.slice(-1797)}`;\n}\n\n/**\n * Owns terminal and destructive session cleanup. Each session is serialized so\n * duplicate role bindings and competing deletion/transition requests cannot\n * run teardown concurrently.\n *\n * Sandbox identity is the session id: retirement stops (or, for deleted\n * sessions, destroys) the sandbox this process holds in the session memo.\n * Sessions opened on another replica (or before a restart) have no memo entry\n * here: stopping them is left to the provider's idle lifecycle, but deleting a\n * materialized session destroys its VM by identity through the configured\n * sandbox. Providers such as E2B keep paused VMs forever, so the row's\n * deletion must never orphan one.\n */\nexport class SessionRetirementCoordinator {\n readonly #invalidateSession: NonNullable<SessionRetirementCoordinatorOptions['invalidateSession']>;\n readonly #sandbox: FactorySandbox | undefined;\n readonly #warn: WarningLogger;\n readonly #destroyTimeoutMs: number;\n readonly #locks = new Map<string, Promise<unknown>>();\n\n constructor(options: SessionRetirementCoordinatorOptions = {}) {\n this.#invalidateSession = options.invalidateSession ?? (() => {});\n this.#sandbox = options.sandbox;\n this.#warn = options.warn ?? ((message, details) => console.warn(`[Mastra Factory] ${message}`, details));\n this.#destroyTimeoutMs = options.destroyTimeoutMs ?? DEFAULT_DESTROY_TIMEOUT_MS;\n }\n\n async retireSession(input: RetireSessionInput): Promise<void> {\n await this.#withSessionLock(input.sessionId, () => this.#retireSession(input));\n }\n\n /**\n * Destroy a session's sandbox so its next message boots a fresh VM from the\n * template, keeping the session row, its conversation, and work-item refs.\n * Skips teardown commands, and works without a memo entry in this process\n * by destroying a detached instance built from the persisted `sandboxId`.\n * A destroy failure keeps the persisted id so a retry can reach the VM.\n */\n async recreateSessionSandbox(input: RecreateSessionSandboxInput): Promise<'recreated' | 'not-found'> {\n return this.#withSessionLock(input.sessionId, () => this.#recreateSessionSandbox(input));\n }\n\n async #withSessionLock<T>(sessionId: string, run: () => Promise<T>): Promise<T> {\n const previous = this.#locks.get(sessionId) ?? Promise.resolve();\n const current = previous.catch(() => {}).then(run);\n this.#locks.set(sessionId, current);\n try {\n return await current;\n } finally {\n if (this.#locks.get(sessionId) === current) this.#locks.delete(sessionId);\n }\n }\n\n async retireWorkItemSessions(options: {\n workItems: Pick<WorkItemsStorage, 'get'>;\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n workItemId: string;\n }): Promise<void> {\n const item = await options.workItems.get({ orgId: options.orgId, id: options.workItemId });\n if (!item) return;\n const sessionIds = [...new Set(Object.values(item.sessions).map(session => session.sessionId))];\n await Promise.all(\n sessionIds.map(sessionId =>\n this.retireSession({\n sourceControl: options.sourceControl,\n orgId: options.orgId,\n sessionId,\n deleteSession: false,\n }),\n ),\n );\n }\n\n async retireProjectRepositorySessions(options: {\n sourceControl: SourceControlStorageHandle;\n workItems?: Pick<WorkItemsStorage, 'clearSessionReferences'>;\n orgId: string;\n projectRepositoryId: string;\n }): Promise<void> {\n const sessions = await options.sourceControl.sessions.listByProjectRepository({\n projectRepositoryId: options.projectRepositoryId,\n });\n await Promise.all(\n sessions.map(session =>\n this.retireSession({\n sourceControl: options.sourceControl,\n ...(options.workItems ? { workItems: options.workItems } : {}),\n orgId: options.orgId,\n sessionId: session.sessionId,\n deleteSession: true,\n }),\n ),\n );\n }\n\n async #retireSession(input: RetireSessionInput): Promise<void> {\n const session = await input.sourceControl.sessions.getBySessionId(input.sessionId);\n if (!session || session.orgId !== input.orgId) return;\n\n try {\n await this.#teardownSessionSandbox(input, session);\n } finally {\n await this.#invalidate(session);\n\n if (input.deleteSession) {\n // Refs first: clearing again is a no-op, but refs on a deleted row would dangle forever.\n await input.workItems?.clearSessionReferences({ orgId: input.orgId, sessionId: input.sessionId });\n await input.sourceControl.sessions.delete(session.id);\n }\n }\n }\n\n async #teardownSessionSandbox(input: RetireSessionInput, session: SourceControlSession): Promise<void> {\n const entry = peekSessionSandbox(session.id);\n if (!entry) {\n await this.#destroyDetachedSandbox(input, session);\n return;\n }\n\n // A session booted from its factory environment tears every repository\n // down in position order, each in its own directory; a failure is logged\n // and the next repository still runs.\n const environmentTeardown = peekSessionEnvironmentTeardown(session.sessionId);\n if (environmentTeardown.length > 0) {\n for (const repo of environmentTeardown) {\n try {\n await runTeardownCommand(requireExec(entry.sandbox), repo.dir, repo.command, {\n timeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,\n });\n } catch (error) {\n this.#warn('Factory teardown command failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n repository: repo.slug,\n error: boundedError(error),\n });\n }\n }\n await this.#releaseSandbox(input, session);\n return;\n }\n\n let projectRepository: ProjectRepository | null | undefined;\n try {\n projectRepository = session.projectRepositoryId\n ? await input.sourceControl.projectRepositories.get({ orgId: input.orgId, id: session.projectRepositoryId })\n : null;\n } catch (error) {\n this.#warn('Factory repository settings could not be loaded for session retirement', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: boundedError(error),\n });\n }\n\n // An unresolved workdir means the sandbox never started in this process:\n // nothing was set up, so there is nothing for a teardown command to undo.\n if (projectRepository?.teardownCommand && entry.workdir) {\n try {\n await runTeardownCommand(requireExec(entry.sandbox), entry.workdir, projectRepository.teardownCommand, {\n timeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,\n });\n } catch (error) {\n this.#warn('Factory teardown command failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: boundedError(error),\n });\n }\n }\n\n await this.#releaseSandbox(input, session);\n }\n\n /**\n * Destroy a deleted session's VM that this process never attached to. Only\n * for sessions that materialized (a persisted `sandboxId`): nothing is\n * started, no teardown command runs — the provider kills by identity.\n */\n async #destroyDetachedSandbox(input: RetireSessionInput, session: SourceControlSession): Promise<void> {\n if (!input.deleteSession || !session.sandboxId || !this.#sandbox) return;\n try {\n await destroyDetachedSessionSandbox({ sandbox: this.#sandbox, session });\n } catch (error) {\n this.#warn('Factory detached session sandbox destroy failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: boundedError(error),\n });\n }\n }\n\n /** Bump the session's workspace generation; a failing unregister is logged, never fatal. */\n async #invalidate(session: SourceControlSession): Promise<void> {\n try {\n await this.#invalidateSession(session.sessionId);\n } catch (error) {\n this.#warn('Factory session workspace cache invalidation failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: boundedError(error),\n });\n }\n }\n\n async #recreateSessionSandbox(input: RecreateSessionSandboxInput): Promise<'recreated' | 'not-found'> {\n const session = await input.sourceControl.sessions.getBySessionId(input.sessionId);\n if (!session || session.orgId !== input.orgId) return 'not-found';\n\n // Bump the generation first so an in-flight start hook for the old\n // instance throws instead of republishing its sandbox id.\n await this.#invalidate(session);\n const entry = peekSessionSandbox(session.id);\n evictSessionSandbox(session.id);\n\n await this.#destroyForRecreate(session, entry?.sandbox);\n await input.sourceControl.sessions.setSandbox({ id: session.id, sandboxId: null, sandboxWorkdir: '' });\n\n // A resolve that ran between the eviction and clearing the id could have\n // built a fresh instance from the old `sandboxId`; drop it and fence it.\n evictSessionSandbox(session.id);\n await this.#invalidate(session);\n return 'recreated';\n }\n\n /**\n * Destroy the session's VM. `_destroy()` silently no-ops on a never-started\n * (`pending`) instance, so pending and detached instances call the\n * provider's `destroy()` directly; a started instance goes through the\n * lifecycle wrapper, bounded so a hung start can't hold the session lock.\n */\n async #destroyForRecreate(session: SourceControlSession, memo: WorkspaceSandbox | undefined): Promise<void> {\n const lifecycle = memo as { _destroy?: () => Promise<void> } | undefined;\n if (memo && memo.status !== 'pending' && lifecycle?._destroy) {\n const destroying = lifecycle._destroy.call(memo);\n if (await this.#settledWithin(destroying)) {\n memo.status = 'destroyed';\n return;\n }\n // Load-bearing: once the hung start settles, this destroy reaps any VM it\n // provisioned. Its rejection must never surface as unhandled.\n destroying.catch(error =>\n this.#warn('Factory sandbox destroy failed after recreate stopped waiting', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n error: boundedError(error),\n }),\n );\n if (!this.#sandbox) {\n throw new Error(\n 'Timed out destroying the session sandbox, and no sandbox provider is configured to retry by identity.',\n );\n }\n await this.#destroyDetachedForRecreate(this.#sandbox, session);\n memo.status = 'destroyed';\n return;\n }\n\n if (memo) {\n await this.#destroyDirectly(memo);\n memo.status = 'destroyed';\n return;\n }\n\n // No instance in this process (restart, or another replica opened it):\n // destroy through a never-started instance carrying the persisted id.\n // Unlike retirement, this runs without a `sandboxId` too: E2B and Docker\n // find the VM by session identity, and Platform's destroy no-ops safely.\n if (this.#sandbox) await this.#destroyDetachedForRecreate(this.#sandbox, session);\n }\n\n async #destroyDetachedForRecreate(sandbox: FactorySandbox, session: SourceControlSession): Promise<void> {\n try {\n await destroyDetachedSessionSandbox({ sandbox, session });\n } catch (error) {\n if (!isSandboxGoneError(error)) throw error;\n }\n }\n\n /** True when the destroy finished in time; false on timeout. Gone-VM errors count as finished. */\n async #settledWithin(destroying: Promise<void>): Promise<boolean> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<false>(resolve => {\n timer = setTimeout(() => resolve(false), this.#destroyTimeoutMs);\n });\n try {\n return await Promise.race([destroying.then(() => true as const), timedOut]);\n } catch (error) {\n if (isSandboxGoneError(error)) return true;\n throw error;\n } finally {\n clearTimeout(timer);\n }\n }\n\n async #destroyDirectly(sandbox: WorkspaceSandbox): Promise<void> {\n try {\n await sandbox.destroy?.();\n } catch (error) {\n if (!isSandboxGoneError(error)) throw error;\n }\n }\n\n async #releaseSandbox(input: RetireSessionInput, session: SourceControlSession): Promise<void> {\n try {\n await releaseSessionSandbox({ sessionId: session.id, destroy: input.deleteSession });\n } catch (error) {\n this.#warn('Factory session sandbox release failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: boundedError(error),\n });\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session-retirement.js","names":[],"sources":["../../src/sandbox/session-retirement.ts"],"sourcesContent":["import type { FactorySandbox, WorkspaceSandbox } from '@mastra/core/workspace';\n\nimport { destroyDetachedSessionSandbox, releaseSessionSandbox } from '../integrations/github/sandbox-release.js';\nimport { DEFAULT_COMMAND_TIMEOUT_MS, runTeardownCommand } from '../integrations/github/sandbox.js';\nimport { peekSessionEnvironmentTeardown } from '../session/environment-state-processor.js';\nimport type {\n ProjectRepository,\n SourceControlSession,\n SourceControlStorageHandle,\n} from '../storage/domains/source-control/base.js';\nimport type { WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport { requireExec } from './materialization.js';\nimport { clearFailedSetupCommands, evictSessionSandbox, peekSessionSandbox } from './session-sandbox.js';\n\ntype WarningLogger = (message: string, details: Record<string, unknown>) => void;\n\nexport interface SessionRetirementCoordinatorOptions {\n invalidateSession?: (sessionId: string) => Promise<void> | void;\n /**\n * The deploy's sandbox configuration. Used to destroy a deleted session's\n * VM by identity when this process never attached to it (after a restart,\n * or on another replica). Recreate uses it the same way.\n */\n sandbox?: FactorySandbox;\n warn?: WarningLogger;\n /** How long recreate waits on a started sandbox's lifecycle destroy before falling back. */\n destroyTimeoutMs?: number;\n}\n\nexport interface RecreateSessionSandboxInput {\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n sessionId: string;\n}\n\nconst DEFAULT_DESTROY_TIMEOUT_MS = 30_000;\n\n/**\n * A provider error meaning the VM is already gone: 410, or a 404 the proxy\n * marks `not_found` (or leaves uncoded). A routing 404 with another code is\n * not proof the VM is gone.\n */\nfunction isSandboxGoneError(error: unknown): boolean {\n if (typeof error !== 'object' || error === null) return false;\n const { status, code } = error as { status?: unknown; code?: unknown };\n if (status === 410) return true;\n return status === 404 && (code === undefined || code === null || code === 'not_found');\n}\n\nexport interface RetireSessionInput {\n sourceControl: SourceControlStorageHandle;\n /** When provided, deleting the session row also strips its work-item refs. */\n workItems?: Pick<WorkItemsStorage, 'clearSessionReferences'>;\n orgId: string;\n sessionId: string;\n deleteSession: boolean;\n}\n\nfunction boundedError(error: unknown): string {\n const detail = error instanceof Error ? error.message : String(error);\n if (detail.length <= 2000) return detail;\n return `${detail.slice(0, 200)}...${detail.slice(-1797)}`;\n}\n\n/**\n * Owns terminal and destructive session cleanup. Each session is serialized so\n * duplicate role bindings and competing deletion/transition requests cannot\n * run teardown concurrently.\n *\n * Sandbox identity is the session id: retirement stops (or, for deleted\n * sessions, destroys) the sandbox this process holds in the session memo.\n * Sessions opened on another replica (or before a restart) have no memo entry\n * here: stopping them is left to the provider's idle lifecycle, but deleting a\n * materialized session destroys its VM by identity through the configured\n * sandbox. Providers such as E2B keep paused VMs forever, so the row's\n * deletion must never orphan one.\n */\nexport class SessionRetirementCoordinator {\n readonly #invalidateSession: NonNullable<SessionRetirementCoordinatorOptions['invalidateSession']>;\n readonly #sandbox: FactorySandbox | undefined;\n readonly #warn: WarningLogger;\n readonly #destroyTimeoutMs: number;\n readonly #locks = new Map<string, Promise<unknown>>();\n\n constructor(options: SessionRetirementCoordinatorOptions = {}) {\n this.#invalidateSession = options.invalidateSession ?? (() => {});\n this.#sandbox = options.sandbox;\n this.#warn = options.warn ?? ((message, details) => console.warn(`[Mastra Factory] ${message}`, details));\n this.#destroyTimeoutMs = options.destroyTimeoutMs ?? DEFAULT_DESTROY_TIMEOUT_MS;\n }\n\n async retireSession(input: RetireSessionInput): Promise<void> {\n await this.#withSessionLock(input.sessionId, () => this.#retireSession(input));\n }\n\n /**\n * Destroy a session's sandbox so its next message boots a fresh VM from the\n * template, keeping the session row, its conversation, and work-item refs.\n * Skips teardown commands, and works without a memo entry in this process\n * by destroying a detached instance built from the persisted `sandboxId`.\n * A destroy failure keeps the persisted id so a retry can reach the VM.\n */\n async recreateSessionSandbox(input: RecreateSessionSandboxInput): Promise<'recreated' | 'not-found'> {\n return this.#withSessionLock(input.sessionId, () => this.#recreateSessionSandbox(input));\n }\n\n /**\n * Replace the session's VM while keeping its sandbox instance, for the\n * session's own agent. A run holds the instance it started with until the\n * run ends, so recreate (which destroys the instance) would leave every\n * later command in that run failing. Here the VM is destroyed and the\n * instance is left `stopped` with no VM id, so the run's next command starts\n * a fresh VM and the start hook sets it up again.\n *\n * `not-here` means this process holds no instance for the session.\n */\n async resetSessionSandbox(input: RecreateSessionSandboxInput): Promise<'reset' | 'not-found' | 'not-here'> {\n return this.#withSessionLock(input.sessionId, () => this.#resetSessionSandbox(input));\n }\n\n async #withSessionLock<T>(sessionId: string, run: () => Promise<T>): Promise<T> {\n const previous = this.#locks.get(sessionId) ?? Promise.resolve();\n const current = previous.catch(() => {}).then(run);\n this.#locks.set(sessionId, current);\n try {\n return await current;\n } finally {\n if (this.#locks.get(sessionId) === current) this.#locks.delete(sessionId);\n }\n }\n\n async retireWorkItemSessions(options: {\n workItems: Pick<WorkItemsStorage, 'get'>;\n sourceControl: SourceControlStorageHandle;\n orgId: string;\n workItemId: string;\n }): Promise<void> {\n const item = await options.workItems.get({ orgId: options.orgId, id: options.workItemId });\n if (!item) return;\n const sessionIds = [...new Set(Object.values(item.sessions).map(session => session.sessionId))];\n await Promise.all(\n sessionIds.map(sessionId =>\n this.retireSession({\n sourceControl: options.sourceControl,\n orgId: options.orgId,\n sessionId,\n deleteSession: false,\n }),\n ),\n );\n }\n\n async retireProjectRepositorySessions(options: {\n sourceControl: SourceControlStorageHandle;\n workItems?: Pick<WorkItemsStorage, 'clearSessionReferences'>;\n orgId: string;\n projectRepositoryId: string;\n }): Promise<void> {\n const sessions = await options.sourceControl.sessions.listByProjectRepository({\n projectRepositoryId: options.projectRepositoryId,\n });\n await Promise.all(\n sessions.map(session =>\n this.retireSession({\n sourceControl: options.sourceControl,\n ...(options.workItems ? { workItems: options.workItems } : {}),\n orgId: options.orgId,\n sessionId: session.sessionId,\n deleteSession: true,\n }),\n ),\n );\n }\n\n async #retireSession(input: RetireSessionInput): Promise<void> {\n const session = await input.sourceControl.sessions.getBySessionId(input.sessionId);\n if (!session || session.orgId !== input.orgId) return;\n\n try {\n await this.#teardownSessionSandbox(input, session);\n } finally {\n await this.#invalidate(session);\n\n if (input.deleteSession) {\n // Refs first: clearing again is a no-op, but refs on a deleted row would dangle forever.\n await input.workItems?.clearSessionReferences({ orgId: input.orgId, sessionId: input.sessionId });\n await input.sourceControl.sessions.delete(session.id);\n }\n }\n }\n\n async #teardownSessionSandbox(input: RetireSessionInput, session: SourceControlSession): Promise<void> {\n const entry = peekSessionSandbox(session.id);\n if (!entry) {\n await this.#destroyDetachedSandbox(input, session);\n return;\n }\n\n // A session booted from its factory environment tears every repository\n // down in position order, each in its own directory; a failure is logged\n // and the next repository still runs.\n const environmentTeardown = peekSessionEnvironmentTeardown(session.sessionId);\n if (environmentTeardown.length > 0) {\n for (const repo of environmentTeardown) {\n try {\n await runTeardownCommand(requireExec(entry.sandbox), repo.dir, repo.command, {\n timeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,\n });\n } catch (error) {\n this.#warn('Factory teardown command failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n repository: repo.slug,\n error: boundedError(error),\n });\n }\n }\n await this.#releaseSandbox(input, session);\n return;\n }\n\n let projectRepository: ProjectRepository | null | undefined;\n try {\n projectRepository = session.projectRepositoryId\n ? await input.sourceControl.projectRepositories.get({ orgId: input.orgId, id: session.projectRepositoryId })\n : null;\n } catch (error) {\n this.#warn('Factory repository settings could not be loaded for session retirement', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: boundedError(error),\n });\n }\n\n // An unresolved workdir means the sandbox never started in this process:\n // nothing was set up, so there is nothing for a teardown command to undo.\n if (projectRepository?.teardownCommand && entry.workdir) {\n try {\n await runTeardownCommand(requireExec(entry.sandbox), entry.workdir, projectRepository.teardownCommand, {\n timeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,\n });\n } catch (error) {\n this.#warn('Factory teardown command failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: boundedError(error),\n });\n }\n }\n\n await this.#releaseSandbox(input, session);\n }\n\n /**\n * Destroy a deleted session's VM that this process never attached to. Only\n * for sessions that materialized (a persisted `sandboxId`): nothing is\n * started, no teardown command runs — the provider kills by identity.\n */\n async #destroyDetachedSandbox(input: RetireSessionInput, session: SourceControlSession): Promise<void> {\n if (!input.deleteSession || !session.sandboxId || !this.#sandbox) return;\n try {\n await destroyDetachedSessionSandbox({ sandbox: this.#sandbox, session });\n } catch (error) {\n this.#warn('Factory detached session sandbox destroy failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: boundedError(error),\n });\n }\n }\n\n /** Bump the session's workspace generation; a failing unregister is logged, never fatal. */\n async #invalidate(session: SourceControlSession): Promise<void> {\n try {\n await this.#invalidateSession(session.sessionId);\n } catch (error) {\n this.#warn('Factory session workspace cache invalidation failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: boundedError(error),\n });\n }\n }\n\n async #recreateSessionSandbox(input: RecreateSessionSandboxInput): Promise<'recreated' | 'not-found'> {\n const session = await input.sourceControl.sessions.getBySessionId(input.sessionId);\n if (!session || session.orgId !== input.orgId) return 'not-found';\n\n // Bump the generation first so an in-flight start hook for the old\n // instance throws instead of republishing its sandbox id.\n await this.#invalidate(session);\n const entry = peekSessionSandbox(session.id);\n evictSessionSandbox(session.id);\n\n await this.#destroyForRecreate(session, entry?.sandbox);\n await input.sourceControl.sessions.setSandbox({ id: session.id, sandboxId: null, sandboxWorkdir: '' });\n\n // A resolve that ran between the eviction and clearing the id could have\n // built a fresh instance from the old `sandboxId`; drop it and fence it.\n evictSessionSandbox(session.id);\n await this.#invalidate(session);\n return 'recreated';\n }\n\n async #resetSessionSandbox(input: RecreateSessionSandboxInput): Promise<'reset' | 'not-found' | 'not-here'> {\n const session = await input.sourceControl.sessions.getBySessionId(input.sessionId);\n if (!session || session.orgId !== input.orgId) return 'not-found';\n const sandbox = peekSessionSandbox(session.id)?.sandbox;\n if (!sandbox) return 'not-here';\n\n // The provider's own destroy, not the lifecycle wrapper: the wrapper marks\n // the instance destroyed for good. Bounded, so a VM that won't answer\n // can't hold the session lock.\n const destroy = async () => {\n if (!(await this.#settledWithin(this.#destroyDirectly(sandbox)))) {\n throw new Error('Timed out destroying the sandbox. Try again.');\n }\n };\n // Destroyed before waiting on a start in flight, so a start hung on the\n // dead VM can't block recovery; its commands fail fast once the VM is gone.\n await destroy();\n const starting = (sandbox as { _startPromise?: Promise<unknown> })._startPromise;\n if (starting) {\n const settled = await this.#settledWithin(\n starting.then(\n () => {},\n () => {},\n ),\n );\n if (!settled) throw new Error('The old sandbox is still starting. Try again.');\n // That start may have found the old VM gone and provisioned another.\n await destroy();\n }\n\n sandbox.status = 'stopped';\n // The new VM runs setup from scratch, including commands that failed on the old one.\n clearFailedSetupCommands(session.id);\n // Re-read: a start that settled while this reset waited may have stored a newer workdir.\n const latest = await input.sourceControl.sessions.getBySessionId(input.sessionId);\n await input.sourceControl.sessions.setSandbox({\n id: session.id,\n sandboxId: null,\n sandboxWorkdir: latest?.sandboxWorkdir ?? session.sandboxWorkdir ?? '',\n });\n return 'reset';\n }\n\n /**\n * Destroy the session's VM. `_destroy()` silently no-ops on a never-started\n * (`pending`) instance, so pending and detached instances call the\n * provider's `destroy()` directly; a started instance goes through the\n * lifecycle wrapper, bounded so a hung start can't hold the session lock.\n */\n async #destroyForRecreate(session: SourceControlSession, memo: WorkspaceSandbox | undefined): Promise<void> {\n const lifecycle = memo as { _destroy?: () => Promise<void> } | undefined;\n if (memo && memo.status !== 'pending' && lifecycle?._destroy) {\n const destroying = lifecycle._destroy.call(memo);\n if (await this.#settledWithin(destroying)) {\n memo.status = 'destroyed';\n return;\n }\n // Load-bearing: once the hung start settles, this destroy reaps any VM it\n // provisioned. Its rejection must never surface as unhandled.\n destroying.catch(error =>\n this.#warn('Factory sandbox destroy failed after recreate stopped waiting', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n error: boundedError(error),\n }),\n );\n if (!this.#sandbox) {\n throw new Error(\n 'Timed out destroying the session sandbox, and no sandbox provider is configured to retry by identity.',\n );\n }\n await this.#destroyDetachedForRecreate(this.#sandbox, session);\n memo.status = 'destroyed';\n return;\n }\n\n if (memo) {\n await this.#destroyDirectly(memo);\n memo.status = 'destroyed';\n return;\n }\n\n // No instance in this process (restart, or another replica opened it):\n // destroy through a never-started instance carrying the persisted id.\n // Unlike retirement, this runs without a `sandboxId` too: E2B and Docker\n // find the VM by session identity, and Platform's destroy no-ops safely.\n if (this.#sandbox) await this.#destroyDetachedForRecreate(this.#sandbox, session);\n }\n\n async #destroyDetachedForRecreate(sandbox: FactorySandbox, session: SourceControlSession): Promise<void> {\n try {\n await destroyDetachedSessionSandbox({ sandbox, session });\n } catch (error) {\n if (!isSandboxGoneError(error)) throw error;\n }\n }\n\n /** True when the destroy finished in time; false on timeout. Gone-VM errors count as finished. */\n async #settledWithin(destroying: Promise<void>): Promise<boolean> {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<false>(resolve => {\n timer = setTimeout(() => resolve(false), this.#destroyTimeoutMs);\n });\n try {\n return await Promise.race([destroying.then(() => true as const), timedOut]);\n } catch (error) {\n if (isSandboxGoneError(error)) return true;\n throw error;\n } finally {\n clearTimeout(timer);\n }\n }\n\n async #destroyDirectly(sandbox: WorkspaceSandbox): Promise<void> {\n try {\n await sandbox.destroy?.();\n } catch (error) {\n if (!isSandboxGoneError(error)) throw error;\n }\n }\n\n async #releaseSandbox(input: RetireSessionInput, session: SourceControlSession): Promise<void> {\n try {\n await releaseSessionSandbox({ sessionId: session.id, destroy: input.deleteSession });\n } catch (error) {\n this.#warn('Factory session sandbox release failed', {\n orgId: session.orgId,\n sessionId: session.sessionId,\n projectRepositoryId: session.projectRepositoryId,\n error: boundedError(error),\n });\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -155,6 +155,8 @@ export declare function evictSessionSandbox(sessionId: string): void;
|
|
|
155
155
|
export declare function __clearSessionSandboxesForTests(): void;
|
|
156
156
|
export declare function recordFailedSetupCommand(sessionId: string, command: string): void;
|
|
157
157
|
export declare function hasFailedSetupCommand(sessionId: string, command: string): boolean;
|
|
158
|
+
/** Forget a session's failed setup commands, so a replacement VM runs them again. */
|
|
159
|
+
export declare function clearFailedSetupCommands(sessionId: string): void;
|
|
158
160
|
/**
|
|
159
161
|
* Build the session setup hook, which factory attaches to the constructed
|
|
160
162
|
* sandbox with `setOnStart`. Runs inside the sandbox start lifecycle on
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-sandbox.d.ts","sourceRoot":"","sources":["../../src/sandbox/session-sandbox.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session-sandbox.d.ts","sourceRoot":"","sources":["../../src/sandbox/session-sandbox.ts"],"names":[],"mappings":"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"}
|
|
@@ -120,6 +120,10 @@ function recordFailedSetupCommand(sessionId, command) {
|
|
|
120
120
|
function hasFailedSetupCommand(sessionId, command) {
|
|
121
121
|
return failedSetupCommands.get(sessionId)?.has(command) ?? false;
|
|
122
122
|
}
|
|
123
|
+
/** Forget a session's failed setup commands, so a replacement VM runs them again. */
|
|
124
|
+
function clearFailedSetupCommands(sessionId) {
|
|
125
|
+
failedSetupCommands.delete(sessionId);
|
|
126
|
+
}
|
|
123
127
|
/**
|
|
124
128
|
* The setup completion marker is a convention shared with the repo templates
|
|
125
129
|
* (`@internal/workspace`): `.mastra-sandbox/setup` beside the checkout,
|
|
@@ -243,6 +247,6 @@ function createEnvironmentSetupHook(run, sessionId, repoFullName, environment) {
|
|
|
243
247
|
};
|
|
244
248
|
}
|
|
245
249
|
//#endregion
|
|
246
|
-
export { __clearSessionSandboxesForTests, createEnvironmentSetupHook, createSessionSetupHook, evictSessionSandbox, getSessionSandbox, hasFailedSetupCommand, peekSessionSandbox, recordFailedSetupCommand, resolveSessionWorkdir, sessionLayout };
|
|
250
|
+
export { __clearSessionSandboxesForTests, clearFailedSetupCommands, createEnvironmentSetupHook, createSessionSetupHook, evictSessionSandbox, getSessionSandbox, hasFailedSetupCommand, peekSessionSandbox, recordFailedSetupCommand, resolveSessionWorkdir, sessionLayout };
|
|
247
251
|
|
|
248
252
|
//# sourceMappingURL=session-sandbox.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-sandbox.js","names":[],"sources":["../../src/sandbox/session-sandbox.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n SETUP_FAILED_MARKER_PATH,\n SETUP_MARKER_PATH,\n WORKSPACE_SETUP_MARKER_PATH,\n repoSetupMarkerPath,\n setupMarkerContent,\n} from '@internal/workspace';\n\nimport type {\n FactorySandbox,\n FactorySandboxContext as CoreFactorySandboxContext,\n MastraSandbox,\n SandboxStartHook,\n WorkspaceSandbox,\n} from '@mastra/core/workspace';\nimport { timedPhase } from '../timing.js';\nimport { deriveLocalWorkdir, deriveRemoteRepoDir, repoDirUnder, repositoryDirectoryName } from './workdir.js';\n\n/**\n * Everything factory knows about a session's sandbox needs: the whole contract\n * between factory and the host's FactorySandbox. Factory owns intent; the\n * provider owns resolving `sessionId` to a runnable VM. Defined in\n * `@mastra/core/workspace` so provider packages can implement it without\n * depending on factory.\n */\nexport type FactorySandboxContext = CoreFactorySandboxContext;\n\n/**\n * The deploy's sandbox configuration: construct a session's sandbox from\n * intent. The sandbox identity is the session id; the provider must honor\n * id-keyed getOrCreate on `start()` (reconnect/resume an existing VM for the\n * id, create otherwise). Construction must be cheap and side-effect-free —\n * VMs are provisioned on `start()` only. Local sandboxes should root their\n * `workingDirectory` at a per-session directory (e.g.\n * `join(root, ctx.sessionId)`); the repo checks out as a subdirectory of it.\n *\n * Forward `ctx.sandboxId` to providers that reattach by physical id so resume\n * reattaches the original VM instead of provisioning a replacement.\n *\n * Returns a `MastraSandbox`, not the bare `WorkspaceSandbox` interface:\n * factory relies on the base class for the start lifecycle and the runtime\n * env, so providers extend it rather than reimplementing the contract.\n *\n * Factory attaches its own session setup to the returned sandbox, so the\n * callback never has to wire it up. A callback may still pass its own\n * `onStart`; it runs after factory's setup, against a prepared workspace.\n *\n * @example\n * ```typescript\n * sandbox: ({ sessionId, sandboxId }) => new E2BSandbox({ id: sessionId, sandboxId })\n * ```\n *\n * A FactorySandbox instance (for example `new E2BFactorySandbox({ apiKey })`)\n * additionally exposes settings and template builds to the environment.\n */\nexport type MastraFactorySandboxConfig = (ctx: FactorySandboxContext) => MastraSandbox;\n\n/**\n * The `sandbox` option of `MastraFactoryConfig`: a FactorySandbox instance\n * (it owns the session sandbox constructor, the repo template and the\n * environment settings schema) or a bare callback, which the factory wraps as a `provider: 'custom'` sandbox with no settings.\n */\nexport type MastraFactorySandboxOption = FactorySandbox | MastraFactorySandboxConfig;\n\n/**\n * What the start hook learned about the setup command before running the\n * session setup, and how to record its completion afterwards.\n */\nexport interface SessionSetupGate {\n /** True when the sandbox already carries a marker for the current setup command. */\n setupDone: boolean;\n /** Write the marker once the setup command succeeded. Best-effort. */\n markSetupDone: () => Promise<void>;\n}\n\n/**\n * The session's setup work, run against a started sandbox on EVERY start.\n * Materialize and checkout are idempotent and must always run (a warm boot\n * still needs its pull); only the setup command consults `gate`.\n */\nexport type SessionSetupRun = (sandbox: WorkspaceSandbox, workdir: string, gate: SessionSetupGate) => Promise<void>;\n\n/** One environment repository as the boot hook needs it: its slug and setup command. */\nexport interface SessionEnvironmentRepository {\n slug: string;\n setupCommand?: string;\n}\n\n/** What the environment hook learned about one repository before the boot runs. */\nexport interface SessionEnvironmentRepositoryGate extends SessionEnvironmentRepository {\n /** `<root>/<repoDir>`, the directory the template cloned this repository into. */\n dir: string;\n gate: SessionSetupGate;\n}\n\n/** The environment hook's view of the workspace: the root, every repository in position order, the workspace gate. */\nexport interface SessionEnvironmentGate {\n /** The workspace root, parent of every repository directory. */\n root: string;\n repos: SessionEnvironmentRepositoryGate[];\n /** Gate for `workspaceSetupCommand`; `setupDone` when there is no command or its marker matches. */\n workspace: SessionSetupGate;\n}\n\n/** The list-form session boot: run on every start, like `SessionSetupRun`. */\nexport type SessionEnvironmentSetupRun = (\n sandbox: WorkspaceSandbox,\n environment: SessionEnvironmentGate,\n) => Promise<void>;\n\n/**\n * Per-process session-id → sandbox instance memo.\n *\n * The provider contract is id-keyed getOrCreate, but provider find-then-create\n * has a real double-create race across independent instances. Memoizing the\n * instance per session makes the base class's per-instance start coalescing\n * apply process-wide per session — the same single-flight scope the fleet's\n * per-binding coalescing provided. Cross-replica races are accepted (the\n * fleet was also per-replica).\n */\ninterface SessionSandboxEntry {\n sandbox: WorkspaceSandbox;\n /**\n * The session's repo checkout root, recorded for passive readers (fs\n * routes, capture, authz). Local sandboxes derive it at construction;\n * remote sandboxes clone into the VM's own home, so it is undefined until\n * `resolveSessionWorkdir` probes the first started VM — passive readers\n * treat an unresolved workdir as \"nothing materialized\".\n */\n workdir?: string;\n /** The session's own repository, the only one `workdir` is ever recorded for. */\n ownRepo: string;\n /** Environment repositories in position order; the session's own repository is among them. */\n repos?: string[];\n}\n\nconst sessionSandboxes = new Map<string, SessionSandboxEntry>();\n\n/** One environment repository as the file readers see it. */\nexport interface SessionLayoutRepository {\n slug: string;\n /** Absolute checkout directory. */\n dir: string;\n /** Path prefix of this checkout under the layout root: empty for a single-repository session, `<dir>/` otherwise. */\n prefix: string;\n}\n\n/**\n * Where a session's files are read from. A session with one repository is\n * rooted at that checkout, as before environments existed; a session with\n * several is rooted at the workspace root with each checkout as a directory\n * beneath it.\n */\nexport interface SessionLayout {\n root: string;\n repos: SessionLayoutRepository[];\n}\n\n/** The layout of a memoized session, `undefined` until its workdir resolved. */\nexport function sessionLayout(entry: SessionSandboxEntry | undefined): SessionLayout | undefined {\n if (!entry?.workdir) return undefined;\n const slugs = entry.repos?.length ? entry.repos : [entry.ownRepo];\n if (slugs.length === 1) {\n return { root: entry.workdir, repos: [{ slug: slugs[0]!, dir: entry.workdir, prefix: '' }] };\n }\n const root = path.posix.dirname(entry.workdir);\n return {\n root,\n repos: slugs.map(slug => {\n const dir = slug.toLowerCase() === entry.ownRepo.toLowerCase() ? entry.workdir! : repoDirUnder(root, slug);\n return { slug, dir, prefix: `${path.posix.basename(dir)}/` };\n }),\n };\n}\n\n/**\n * Get the session's memoized sandbox entry, constructing (and memoizing) it on\n * first access. Construction is cheap and side-effect-free by contract; VMs\n * are provisioned on `start()` only. Local sandboxes get their workdir here;\n * remote workdirs are a runtime fact of the VM, resolved on first start.\n */\nexport function getSessionSandbox(\n sessionId: string,\n repoFullName: string,\n construct: () => WorkspaceSandbox,\n repos?: string[],\n): SessionSandboxEntry {\n const existing = sessionSandboxes.get(sessionId);\n if (existing) {\n // The environment can change between boots; the layout follows the latest.\n if (repos) existing.repos = repos;\n return existing;\n }\n const sandbox = construct();\n const local = deriveLocalWorkdir(sandbox, repoFullName);\n const entry: SessionSandboxEntry = {\n sandbox,\n ownRepo: repoFullName,\n ...(local ? { workdir: local } : {}),\n ...(repos ? { repos } : {}),\n };\n sessionSandboxes.set(sessionId, entry);\n return entry;\n}\n\n/**\n * Resolve (and memoize on the session entry) the session's repo checkout\n * root. Local sandboxes answer synchronously from their configured\n * `workingDirectory`; remote sandboxes clone into the VM's own default cwd,\n * so the first resolution probes it with one `pwd` — the VM tells us where\n * home is, we never invent a path. Calling this against a stopped sandbox\n * lazily starts it (the probe is a command), so passive readers must peek\n * `entry.workdir` instead. The memo is the session's own checkout; another\n * environment repository lives beside it under the same root, so it is\n * answered from the memo's parent without a second probe.\n */\nexport async function resolveSessionWorkdir(\n sessionId: string,\n sandbox: WorkspaceSandbox,\n repoFullName: string,\n): Promise<string> {\n const entry = sessionSandboxes.get(sessionId);\n const memo = entry && entry.sandbox === sandbox ? entry : undefined;\n if (memo && memo.ownRepo !== repoFullName) {\n // Passive readers take `workdir` as the session's own checkout, so a\n // sibling never lands in the memo: resolve the own checkout (cold memo\n // included) and answer the sibling beside it.\n const own = await resolveSessionWorkdir(sessionId, sandbox, memo.ownRepo);\n return repoDirUnder(path.posix.dirname(own), repoFullName);\n }\n if (memo?.workdir) return memo.workdir;\n const workdir =\n deriveLocalWorkdir(sandbox, repoFullName) ??\n deriveRemoteRepoDir(sandbox, repoFullName) ??\n repoDirUnder(await probeHome(sandbox), repoFullName);\n if (memo) memo.workdir = workdir;\n return workdir;\n}\n\n/** One `pwd` in the VM's default shell cwd — its home dir, by provider convention. */\nasync function probeHome(sandbox: WorkspaceSandbox): Promise<string> {\n if (!sandbox.executeCommand) {\n throw new Error(`Sandbox '${sandbox.id}' cannot resolve its workdir: no executeCommand implementation`);\n }\n const probe = await sandbox.executeCommand('pwd');\n const home = probe.stdout.trim().split('\\n').pop()?.trim() ?? '';\n if (probe.exitCode !== 0 || !home.startsWith('/')) {\n throw new Error(\n `Sandbox '${sandbox.id}' default cwd probe failed (exit ${probe.exitCode}): ${\n probe.stderr.trim() || probe.stdout.trim() || 'empty output'\n }`,\n );\n }\n return home;\n}\n\n/**\n * The session's memoized sandbox (and its workdir) when one was already\n * constructed in this process, else undefined. Never constructs — passive\n * read paths use this so browsing files cannot provision a VM.\n */\nexport function peekSessionSandbox(sessionId: string): SessionSandboxEntry | undefined {\n return sessionSandboxes.get(sessionId);\n}\n\n/** Drop the memoized instance (on stop/destroy/retirement or construction failure). */\nexport function evictSessionSandbox(sessionId: string): void {\n sessionSandboxes.delete(sessionId);\n failedSetupCommands.delete(sessionId);\n}\n\n/** Test-only: reset the process-wide memo between tests. */\nexport function __clearSessionSandboxesForTests(): void {\n sessionSandboxes.clear();\n failedSetupCommands.clear();\n}\n\n/**\n * Setup commands that already failed once for a session. The first failure\n * fails the start loudly — the agent sees the real error in the tool result\n * that triggered it. Recording it lets the next start skip the known-bad\n * command instead of wedging the session behind a permanently failing\n * onStart: clone and checkout still run, and the agent can fix or re-run\n * the setup itself. Keyed by the exact command, one entry per failed command, so an\n * edited setup command runs fresh. In-memory only — a server restart re-runs the (idempotent)\n * setup.\n */\nconst failedSetupCommands = new Map<string, Set<string>>();\n\nexport function recordFailedSetupCommand(sessionId: string, command: string): void {\n const failed = failedSetupCommands.get(sessionId) ?? new Set<string>();\n failed.add(command);\n failedSetupCommands.set(sessionId, failed);\n}\n\nexport function hasFailedSetupCommand(sessionId: string, command: string): boolean {\n return failedSetupCommands.get(sessionId)?.has(command) ?? false;\n}\n\n/**\n * The setup completion marker is a convention shared with the repo templates\n * (`@internal/workspace`): `.mastra-sandbox/setup` beside the checkout,\n * containing a digest of the setup commands. Templates write it as their last\n * build step, so a sandbox booted from a warm image already carries it; the\n * start hook writes it after a successful runtime setup. It is a skip cache,\n * not a correctness mechanism: the setup command is assumed idempotent, and a\n * missing or mismatched marker only re-runs it.\n *\n * The working directory is the parent of the repo dir, which is also the\n * template's build cwd, so this is the file the template's marker step wrote.\n */\nfunction markerShellPath(workdir: string): string {\n return `${path.posix.dirname(workdir)}/${SETUP_MARKER_PATH}`;\n}\n\nasync function markerMatches(sandbox: WorkspaceSandbox, workdir: string, content: string): Promise<boolean> {\n // The marker sits beside the checkout, not inside it, so it can outlive a\n // removed checkout (a wiped local session dir, a recovered VM). Trust it\n // only when the checkout it describes exists and it names this command.\n const marker = markerShellPath(workdir);\n const probe = await sandbox.executeCommand!(\n `test -d \"${workdir}/.git\" && test -f \"${marker}\" && [ \"$(cat \"${marker}\")\" = \"${content}\" ]`,\n );\n return probe.exitCode === 0;\n}\n\nasync function writeMarker(sandbox: WorkspaceSandbox, workdir: string, content: string): Promise<void> {\n // Best-effort: a missing marker only re-runs the idempotent setup later.\n const marker = markerShellPath(workdir);\n await sandbox.executeCommand!(`mkdir -p \"$(dirname \"${marker}\")\" && printf '%s' '${content}' > \"${marker}\"`).catch(\n () => {},\n );\n}\n\n/**\n * Build the session setup hook, which factory attaches to the constructed\n * sandbox with `setOnStart`. Runs inside the sandbox start lifecycle on\n * every start, fresh VM or reconnect: materialize and checkout always run,\n * and the setup command runs unless the sandbox already carries the marker\n * for it (a warm template image, or an earlier successful start). Throwing\n * fails `start()` loudly; core treats onStart errors as fatal.\n */\nexport function createSessionSetupHook(\n run: SessionSetupRun,\n sessionId: string,\n repoFullName: string,\n setupCommand: string | undefined,\n): SandboxStartHook {\n // No command, no marker: nothing to gate.\n const marker = setupCommand?.trim() ? setupMarkerContent(setupCommand) : undefined;\n return async ({ sandbox }) => {\n if (!sandbox.executeCommand) {\n throw new Error(`Sandbox '${sandbox.id}' cannot run the session setup: no executeCommand implementation`);\n }\n // Resolved from the live instance (the hook runs inside `start()`, so the\n // VM is up) and memoized on the session entry for passive readers.\n const workdir = await resolveSessionWorkdir(sessionId, sandbox, repoFullName);\n // Probed before materialize so a wiped checkout reads as \"not done\" even\n // though materialize is about to restore it.\n const setupDone = marker\n ? await timedPhase('workspace.setup-marker', () => markerMatches(sandbox, workdir, marker))\n : true;\n if (marker) {\n process.stderr.write(\n `[factory:setup] ${setupDone ? 'marker matches, skipping setup command' : 'no matching marker, setup command will run'} (${markerShellPath(workdir)})\\n`,\n );\n }\n await run(sandbox, workdir, {\n setupDone,\n markSetupDone: () => (marker ? writeMarker(sandbox, workdir, marker) : Promise.resolve()),\n });\n };\n}\n\n/**\n * Shell-quote one marker file's content. Marker contents are digests, but\n * the quoting keeps an edited command's digest from ever reaching the shell\n * unquoted.\n */\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\n/**\n * The list-form marker probe: trust a repository's marker only when its\n * checkout exists, the marker names this command, and the template did not\n * record the repository in `setup-failed` (a failed per-repo setup still\n * writes the marker because every step ran; the failure list says it failed).\n */\nasync function repoMarkerMatches(\n sandbox: WorkspaceSandbox,\n root: string,\n repoDir: string,\n content: string,\n): Promise<boolean> {\n const marker = `${root}/${repoSetupMarkerPath(repoDir)}`;\n const failed = `${root}/${SETUP_FAILED_MARKER_PATH}`;\n const probe = await sandbox.executeCommand!(\n `test -d \"${root}/${repoDir}/.git\" && test -f \"${marker}\" && [ \"$(cat \"${marker}\")\" = ${shellQuote(content)} ] && ! grep -qxF -- ${shellQuote(repoDir)} \"${failed}\" 2>/dev/null`,\n );\n return probe.exitCode === 0;\n}\n\n/** Write a repository's marker and drop it from `setup-failed`. Best-effort, like `writeMarker`. */\nasync function writeRepoMarker(\n sandbox: WorkspaceSandbox,\n root: string,\n repoDir: string,\n content: string,\n): Promise<void> {\n const marker = `${root}/${repoSetupMarkerPath(repoDir)}`;\n const failed = `${root}/${SETUP_FAILED_MARKER_PATH}`;\n await sandbox.executeCommand!(\n `mkdir -p \"$(dirname \"${marker}\")\" && printf '%s' ${shellQuote(content)} > \"${marker}\" && { test -f \"${failed}\" && { grep -vxF -- ${shellQuote(repoDir)} \"${failed}\" > \"${failed}.tmp\" || true; } && mv \"${failed}.tmp\" \"${failed}\" || true; }`,\n ).catch(() => {});\n}\n\nasync function workspaceMarkerMatches(sandbox: WorkspaceSandbox, root: string, content: string): Promise<boolean> {\n const marker = `${root}/${WORKSPACE_SETUP_MARKER_PATH}`;\n const probe = await sandbox.executeCommand!(\n `test -f \"${marker}\" && [ \"$(cat \"${marker}\")\" = ${shellQuote(content)} ]`,\n );\n return probe.exitCode === 0;\n}\n\nasync function writeWorkspaceMarker(sandbox: WorkspaceSandbox, root: string, content: string): Promise<void> {\n const marker = `${root}/${WORKSPACE_SETUP_MARKER_PATH}`;\n await sandbox.executeCommand!(\n `mkdir -p \"$(dirname \"${marker}\")\" && printf '%s' ${shellQuote(content)} > \"${marker}\"`,\n ).catch(() => {});\n}\n\n/**\n * The list-form start hook for a session whose factory has an environment:\n * the same lifecycle as `createSessionSetupHook`, with one gate per\n * repository plus one for the workspace setup command. The markers are the\n * ones the multi-repo templates write (`.mastra-sandbox/repos/<repoDir>`,\n * `.mastra-sandbox/workspace-setup`, `.mastra-sandbox/setup-failed`), all\n * under the workspace root, which is the parent of the primary repository's\n * directory: `resolveSessionWorkdir` keeps answering that directory so the\n * PR tools and passive readers see the primary checkout, and the root is\n * derived from it rather than probed twice.\n *\n * `repos` is in position order and must include the primary (`repoFullName`).\n */\nexport function createEnvironmentSetupHook(\n run: SessionEnvironmentSetupRun,\n sessionId: string,\n repoFullName: string,\n environment: { repos: SessionEnvironmentRepository[]; workspaceSetupCommand?: string },\n): SandboxStartHook {\n const workspaceCommand = environment.workspaceSetupCommand?.trim() ? environment.workspaceSetupCommand : undefined;\n const workspaceMarker = workspaceCommand ? setupMarkerContent(workspaceCommand) : undefined;\n return async ({ sandbox }) => {\n if (!sandbox.executeCommand) {\n throw new Error(`Sandbox '${sandbox.id}' cannot run the session setup: no executeCommand implementation`);\n }\n const workdir = await resolveSessionWorkdir(sessionId, sandbox, repoFullName);\n const root = path.posix.dirname(workdir);\n const repos: SessionEnvironmentRepositoryGate[] = [];\n for (const repo of environment.repos) {\n const repoDir = repositoryDirectoryName(repo.slug);\n const marker = repo.setupCommand?.trim() ? setupMarkerContent(repo.setupCommand) : undefined;\n const setupDone = marker\n ? await timedPhase(`workspace.setup-marker(${repoDir})`, () =>\n repoMarkerMatches(sandbox, root, repoDir, marker),\n )\n : true;\n if (marker) {\n process.stderr.write(\n `[factory:setup] ${repo.slug}: ${setupDone ? 'marker matches, skipping setup command' : 'no matching marker, setup command will run'} (${root}/${repoSetupMarkerPath(repoDir)})\\n`,\n );\n }\n repos.push({\n ...repo,\n dir: `${root}/${repoDir}`,\n gate: {\n setupDone,\n markSetupDone: () => (marker ? writeRepoMarker(sandbox, root, repoDir, marker) : Promise.resolve()),\n },\n });\n }\n const workspaceDone = workspaceMarker\n ? await timedPhase('workspace.setup-marker(workspace)', () =>\n workspaceMarkerMatches(sandbox, root, workspaceMarker),\n )\n : true;\n await run(sandbox, {\n root,\n repos,\n workspace: {\n setupDone: workspaceDone,\n markSetupDone: () =>\n workspaceMarker ? writeWorkspaceMarker(sandbox, root, workspaceMarker) : Promise.resolve(),\n },\n });\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session-sandbox.js","names":[],"sources":["../../src/sandbox/session-sandbox.ts"],"sourcesContent":["import path from 'node:path';\nimport {\n SETUP_FAILED_MARKER_PATH,\n SETUP_MARKER_PATH,\n WORKSPACE_SETUP_MARKER_PATH,\n repoSetupMarkerPath,\n setupMarkerContent,\n} from '@internal/workspace';\n\nimport type {\n FactorySandbox,\n FactorySandboxContext as CoreFactorySandboxContext,\n MastraSandbox,\n SandboxStartHook,\n WorkspaceSandbox,\n} from '@mastra/core/workspace';\nimport { timedPhase } from '../timing.js';\nimport { deriveLocalWorkdir, deriveRemoteRepoDir, repoDirUnder, repositoryDirectoryName } from './workdir.js';\n\n/**\n * Everything factory knows about a session's sandbox needs: the whole contract\n * between factory and the host's FactorySandbox. Factory owns intent; the\n * provider owns resolving `sessionId` to a runnable VM. Defined in\n * `@mastra/core/workspace` so provider packages can implement it without\n * depending on factory.\n */\nexport type FactorySandboxContext = CoreFactorySandboxContext;\n\n/**\n * The deploy's sandbox configuration: construct a session's sandbox from\n * intent. The sandbox identity is the session id; the provider must honor\n * id-keyed getOrCreate on `start()` (reconnect/resume an existing VM for the\n * id, create otherwise). Construction must be cheap and side-effect-free —\n * VMs are provisioned on `start()` only. Local sandboxes should root their\n * `workingDirectory` at a per-session directory (e.g.\n * `join(root, ctx.sessionId)`); the repo checks out as a subdirectory of it.\n *\n * Forward `ctx.sandboxId` to providers that reattach by physical id so resume\n * reattaches the original VM instead of provisioning a replacement.\n *\n * Returns a `MastraSandbox`, not the bare `WorkspaceSandbox` interface:\n * factory relies on the base class for the start lifecycle and the runtime\n * env, so providers extend it rather than reimplementing the contract.\n *\n * Factory attaches its own session setup to the returned sandbox, so the\n * callback never has to wire it up. A callback may still pass its own\n * `onStart`; it runs after factory's setup, against a prepared workspace.\n *\n * @example\n * ```typescript\n * sandbox: ({ sessionId, sandboxId }) => new E2BSandbox({ id: sessionId, sandboxId })\n * ```\n *\n * A FactorySandbox instance (for example `new E2BFactorySandbox({ apiKey })`)\n * additionally exposes settings and template builds to the environment.\n */\nexport type MastraFactorySandboxConfig = (ctx: FactorySandboxContext) => MastraSandbox;\n\n/**\n * The `sandbox` option of `MastraFactoryConfig`: a FactorySandbox instance\n * (it owns the session sandbox constructor, the repo template and the\n * environment settings schema) or a bare callback, which the factory wraps as a `provider: 'custom'` sandbox with no settings.\n */\nexport type MastraFactorySandboxOption = FactorySandbox | MastraFactorySandboxConfig;\n\n/**\n * What the start hook learned about the setup command before running the\n * session setup, and how to record its completion afterwards.\n */\nexport interface SessionSetupGate {\n /** True when the sandbox already carries a marker for the current setup command. */\n setupDone: boolean;\n /** Write the marker once the setup command succeeded. Best-effort. */\n markSetupDone: () => Promise<void>;\n}\n\n/**\n * The session's setup work, run against a started sandbox on EVERY start.\n * Materialize and checkout are idempotent and must always run (a warm boot\n * still needs its pull); only the setup command consults `gate`.\n */\nexport type SessionSetupRun = (sandbox: WorkspaceSandbox, workdir: string, gate: SessionSetupGate) => Promise<void>;\n\n/** One environment repository as the boot hook needs it: its slug and setup command. */\nexport interface SessionEnvironmentRepository {\n slug: string;\n setupCommand?: string;\n}\n\n/** What the environment hook learned about one repository before the boot runs. */\nexport interface SessionEnvironmentRepositoryGate extends SessionEnvironmentRepository {\n /** `<root>/<repoDir>`, the directory the template cloned this repository into. */\n dir: string;\n gate: SessionSetupGate;\n}\n\n/** The environment hook's view of the workspace: the root, every repository in position order, the workspace gate. */\nexport interface SessionEnvironmentGate {\n /** The workspace root, parent of every repository directory. */\n root: string;\n repos: SessionEnvironmentRepositoryGate[];\n /** Gate for `workspaceSetupCommand`; `setupDone` when there is no command or its marker matches. */\n workspace: SessionSetupGate;\n}\n\n/** The list-form session boot: run on every start, like `SessionSetupRun`. */\nexport type SessionEnvironmentSetupRun = (\n sandbox: WorkspaceSandbox,\n environment: SessionEnvironmentGate,\n) => Promise<void>;\n\n/**\n * Per-process session-id → sandbox instance memo.\n *\n * The provider contract is id-keyed getOrCreate, but provider find-then-create\n * has a real double-create race across independent instances. Memoizing the\n * instance per session makes the base class's per-instance start coalescing\n * apply process-wide per session — the same single-flight scope the fleet's\n * per-binding coalescing provided. Cross-replica races are accepted (the\n * fleet was also per-replica).\n */\ninterface SessionSandboxEntry {\n sandbox: WorkspaceSandbox;\n /**\n * The session's repo checkout root, recorded for passive readers (fs\n * routes, capture, authz). Local sandboxes derive it at construction;\n * remote sandboxes clone into the VM's own home, so it is undefined until\n * `resolveSessionWorkdir` probes the first started VM — passive readers\n * treat an unresolved workdir as \"nothing materialized\".\n */\n workdir?: string;\n /** The session's own repository, the only one `workdir` is ever recorded for. */\n ownRepo: string;\n /** Environment repositories in position order; the session's own repository is among them. */\n repos?: string[];\n}\n\nconst sessionSandboxes = new Map<string, SessionSandboxEntry>();\n\n/** One environment repository as the file readers see it. */\nexport interface SessionLayoutRepository {\n slug: string;\n /** Absolute checkout directory. */\n dir: string;\n /** Path prefix of this checkout under the layout root: empty for a single-repository session, `<dir>/` otherwise. */\n prefix: string;\n}\n\n/**\n * Where a session's files are read from. A session with one repository is\n * rooted at that checkout, as before environments existed; a session with\n * several is rooted at the workspace root with each checkout as a directory\n * beneath it.\n */\nexport interface SessionLayout {\n root: string;\n repos: SessionLayoutRepository[];\n}\n\n/** The layout of a memoized session, `undefined` until its workdir resolved. */\nexport function sessionLayout(entry: SessionSandboxEntry | undefined): SessionLayout | undefined {\n if (!entry?.workdir) return undefined;\n const slugs = entry.repos?.length ? entry.repos : [entry.ownRepo];\n if (slugs.length === 1) {\n return { root: entry.workdir, repos: [{ slug: slugs[0]!, dir: entry.workdir, prefix: '' }] };\n }\n const root = path.posix.dirname(entry.workdir);\n return {\n root,\n repos: slugs.map(slug => {\n const dir = slug.toLowerCase() === entry.ownRepo.toLowerCase() ? entry.workdir! : repoDirUnder(root, slug);\n return { slug, dir, prefix: `${path.posix.basename(dir)}/` };\n }),\n };\n}\n\n/**\n * Get the session's memoized sandbox entry, constructing (and memoizing) it on\n * first access. Construction is cheap and side-effect-free by contract; VMs\n * are provisioned on `start()` only. Local sandboxes get their workdir here;\n * remote workdirs are a runtime fact of the VM, resolved on first start.\n */\nexport function getSessionSandbox(\n sessionId: string,\n repoFullName: string,\n construct: () => WorkspaceSandbox,\n repos?: string[],\n): SessionSandboxEntry {\n const existing = sessionSandboxes.get(sessionId);\n if (existing) {\n // The environment can change between boots; the layout follows the latest.\n if (repos) existing.repos = repos;\n return existing;\n }\n const sandbox = construct();\n const local = deriveLocalWorkdir(sandbox, repoFullName);\n const entry: SessionSandboxEntry = {\n sandbox,\n ownRepo: repoFullName,\n ...(local ? { workdir: local } : {}),\n ...(repos ? { repos } : {}),\n };\n sessionSandboxes.set(sessionId, entry);\n return entry;\n}\n\n/**\n * Resolve (and memoize on the session entry) the session's repo checkout\n * root. Local sandboxes answer synchronously from their configured\n * `workingDirectory`; remote sandboxes clone into the VM's own default cwd,\n * so the first resolution probes it with one `pwd` — the VM tells us where\n * home is, we never invent a path. Calling this against a stopped sandbox\n * lazily starts it (the probe is a command), so passive readers must peek\n * `entry.workdir` instead. The memo is the session's own checkout; another\n * environment repository lives beside it under the same root, so it is\n * answered from the memo's parent without a second probe.\n */\nexport async function resolveSessionWorkdir(\n sessionId: string,\n sandbox: WorkspaceSandbox,\n repoFullName: string,\n): Promise<string> {\n const entry = sessionSandboxes.get(sessionId);\n const memo = entry && entry.sandbox === sandbox ? entry : undefined;\n if (memo && memo.ownRepo !== repoFullName) {\n // Passive readers take `workdir` as the session's own checkout, so a\n // sibling never lands in the memo: resolve the own checkout (cold memo\n // included) and answer the sibling beside it.\n const own = await resolveSessionWorkdir(sessionId, sandbox, memo.ownRepo);\n return repoDirUnder(path.posix.dirname(own), repoFullName);\n }\n if (memo?.workdir) return memo.workdir;\n const workdir =\n deriveLocalWorkdir(sandbox, repoFullName) ??\n deriveRemoteRepoDir(sandbox, repoFullName) ??\n repoDirUnder(await probeHome(sandbox), repoFullName);\n if (memo) memo.workdir = workdir;\n return workdir;\n}\n\n/** One `pwd` in the VM's default shell cwd — its home dir, by provider convention. */\nasync function probeHome(sandbox: WorkspaceSandbox): Promise<string> {\n if (!sandbox.executeCommand) {\n throw new Error(`Sandbox '${sandbox.id}' cannot resolve its workdir: no executeCommand implementation`);\n }\n const probe = await sandbox.executeCommand('pwd');\n const home = probe.stdout.trim().split('\\n').pop()?.trim() ?? '';\n if (probe.exitCode !== 0 || !home.startsWith('/')) {\n throw new Error(\n `Sandbox '${sandbox.id}' default cwd probe failed (exit ${probe.exitCode}): ${\n probe.stderr.trim() || probe.stdout.trim() || 'empty output'\n }`,\n );\n }\n return home;\n}\n\n/**\n * The session's memoized sandbox (and its workdir) when one was already\n * constructed in this process, else undefined. Never constructs — passive\n * read paths use this so browsing files cannot provision a VM.\n */\nexport function peekSessionSandbox(sessionId: string): SessionSandboxEntry | undefined {\n return sessionSandboxes.get(sessionId);\n}\n\n/** Drop the memoized instance (on stop/destroy/retirement or construction failure). */\nexport function evictSessionSandbox(sessionId: string): void {\n sessionSandboxes.delete(sessionId);\n failedSetupCommands.delete(sessionId);\n}\n\n/** Test-only: reset the process-wide memo between tests. */\nexport function __clearSessionSandboxesForTests(): void {\n sessionSandboxes.clear();\n failedSetupCommands.clear();\n}\n\n/**\n * Setup commands that already failed once for a session. The first failure\n * fails the start loudly — the agent sees the real error in the tool result\n * that triggered it. Recording it lets the next start skip the known-bad\n * command instead of wedging the session behind a permanently failing\n * onStart: clone and checkout still run, and the agent can fix or re-run\n * the setup itself. Keyed by the exact command, one entry per failed command, so an\n * edited setup command runs fresh. In-memory only — a server restart re-runs the (idempotent)\n * setup.\n */\nconst failedSetupCommands = new Map<string, Set<string>>();\n\nexport function recordFailedSetupCommand(sessionId: string, command: string): void {\n const failed = failedSetupCommands.get(sessionId) ?? new Set<string>();\n failed.add(command);\n failedSetupCommands.set(sessionId, failed);\n}\n\nexport function hasFailedSetupCommand(sessionId: string, command: string): boolean {\n return failedSetupCommands.get(sessionId)?.has(command) ?? false;\n}\n\n/** Forget a session's failed setup commands, so a replacement VM runs them again. */\nexport function clearFailedSetupCommands(sessionId: string): void {\n failedSetupCommands.delete(sessionId);\n}\n\n/**\n * The setup completion marker is a convention shared with the repo templates\n * (`@internal/workspace`): `.mastra-sandbox/setup` beside the checkout,\n * containing a digest of the setup commands. Templates write it as their last\n * build step, so a sandbox booted from a warm image already carries it; the\n * start hook writes it after a successful runtime setup. It is a skip cache,\n * not a correctness mechanism: the setup command is assumed idempotent, and a\n * missing or mismatched marker only re-runs it.\n *\n * The working directory is the parent of the repo dir, which is also the\n * template's build cwd, so this is the file the template's marker step wrote.\n */\nfunction markerShellPath(workdir: string): string {\n return `${path.posix.dirname(workdir)}/${SETUP_MARKER_PATH}`;\n}\n\nasync function markerMatches(sandbox: WorkspaceSandbox, workdir: string, content: string): Promise<boolean> {\n // The marker sits beside the checkout, not inside it, so it can outlive a\n // removed checkout (a wiped local session dir, a recovered VM). Trust it\n // only when the checkout it describes exists and it names this command.\n const marker = markerShellPath(workdir);\n const probe = await sandbox.executeCommand!(\n `test -d \"${workdir}/.git\" && test -f \"${marker}\" && [ \"$(cat \"${marker}\")\" = \"${content}\" ]`,\n );\n return probe.exitCode === 0;\n}\n\nasync function writeMarker(sandbox: WorkspaceSandbox, workdir: string, content: string): Promise<void> {\n // Best-effort: a missing marker only re-runs the idempotent setup later.\n const marker = markerShellPath(workdir);\n await sandbox.executeCommand!(`mkdir -p \"$(dirname \"${marker}\")\" && printf '%s' '${content}' > \"${marker}\"`).catch(\n () => {},\n );\n}\n\n/**\n * Build the session setup hook, which factory attaches to the constructed\n * sandbox with `setOnStart`. Runs inside the sandbox start lifecycle on\n * every start, fresh VM or reconnect: materialize and checkout always run,\n * and the setup command runs unless the sandbox already carries the marker\n * for it (a warm template image, or an earlier successful start). Throwing\n * fails `start()` loudly; core treats onStart errors as fatal.\n */\nexport function createSessionSetupHook(\n run: SessionSetupRun,\n sessionId: string,\n repoFullName: string,\n setupCommand: string | undefined,\n): SandboxStartHook {\n // No command, no marker: nothing to gate.\n const marker = setupCommand?.trim() ? setupMarkerContent(setupCommand) : undefined;\n return async ({ sandbox }) => {\n if (!sandbox.executeCommand) {\n throw new Error(`Sandbox '${sandbox.id}' cannot run the session setup: no executeCommand implementation`);\n }\n // Resolved from the live instance (the hook runs inside `start()`, so the\n // VM is up) and memoized on the session entry for passive readers.\n const workdir = await resolveSessionWorkdir(sessionId, sandbox, repoFullName);\n // Probed before materialize so a wiped checkout reads as \"not done\" even\n // though materialize is about to restore it.\n const setupDone = marker\n ? await timedPhase('workspace.setup-marker', () => markerMatches(sandbox, workdir, marker))\n : true;\n if (marker) {\n process.stderr.write(\n `[factory:setup] ${setupDone ? 'marker matches, skipping setup command' : 'no matching marker, setup command will run'} (${markerShellPath(workdir)})\\n`,\n );\n }\n await run(sandbox, workdir, {\n setupDone,\n markSetupDone: () => (marker ? writeMarker(sandbox, workdir, marker) : Promise.resolve()),\n });\n };\n}\n\n/**\n * Shell-quote one marker file's content. Marker contents are digests, but\n * the quoting keeps an edited command's digest from ever reaching the shell\n * unquoted.\n */\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n\n/**\n * The list-form marker probe: trust a repository's marker only when its\n * checkout exists, the marker names this command, and the template did not\n * record the repository in `setup-failed` (a failed per-repo setup still\n * writes the marker because every step ran; the failure list says it failed).\n */\nasync function repoMarkerMatches(\n sandbox: WorkspaceSandbox,\n root: string,\n repoDir: string,\n content: string,\n): Promise<boolean> {\n const marker = `${root}/${repoSetupMarkerPath(repoDir)}`;\n const failed = `${root}/${SETUP_FAILED_MARKER_PATH}`;\n const probe = await sandbox.executeCommand!(\n `test -d \"${root}/${repoDir}/.git\" && test -f \"${marker}\" && [ \"$(cat \"${marker}\")\" = ${shellQuote(content)} ] && ! grep -qxF -- ${shellQuote(repoDir)} \"${failed}\" 2>/dev/null`,\n );\n return probe.exitCode === 0;\n}\n\n/** Write a repository's marker and drop it from `setup-failed`. Best-effort, like `writeMarker`. */\nasync function writeRepoMarker(\n sandbox: WorkspaceSandbox,\n root: string,\n repoDir: string,\n content: string,\n): Promise<void> {\n const marker = `${root}/${repoSetupMarkerPath(repoDir)}`;\n const failed = `${root}/${SETUP_FAILED_MARKER_PATH}`;\n await sandbox.executeCommand!(\n `mkdir -p \"$(dirname \"${marker}\")\" && printf '%s' ${shellQuote(content)} > \"${marker}\" && { test -f \"${failed}\" && { grep -vxF -- ${shellQuote(repoDir)} \"${failed}\" > \"${failed}.tmp\" || true; } && mv \"${failed}.tmp\" \"${failed}\" || true; }`,\n ).catch(() => {});\n}\n\nasync function workspaceMarkerMatches(sandbox: WorkspaceSandbox, root: string, content: string): Promise<boolean> {\n const marker = `${root}/${WORKSPACE_SETUP_MARKER_PATH}`;\n const probe = await sandbox.executeCommand!(\n `test -f \"${marker}\" && [ \"$(cat \"${marker}\")\" = ${shellQuote(content)} ]`,\n );\n return probe.exitCode === 0;\n}\n\nasync function writeWorkspaceMarker(sandbox: WorkspaceSandbox, root: string, content: string): Promise<void> {\n const marker = `${root}/${WORKSPACE_SETUP_MARKER_PATH}`;\n await sandbox.executeCommand!(\n `mkdir -p \"$(dirname \"${marker}\")\" && printf '%s' ${shellQuote(content)} > \"${marker}\"`,\n ).catch(() => {});\n}\n\n/**\n * The list-form start hook for a session whose factory has an environment:\n * the same lifecycle as `createSessionSetupHook`, with one gate per\n * repository plus one for the workspace setup command. The markers are the\n * ones the multi-repo templates write (`.mastra-sandbox/repos/<repoDir>`,\n * `.mastra-sandbox/workspace-setup`, `.mastra-sandbox/setup-failed`), all\n * under the workspace root, which is the parent of the primary repository's\n * directory: `resolveSessionWorkdir` keeps answering that directory so the\n * PR tools and passive readers see the primary checkout, and the root is\n * derived from it rather than probed twice.\n *\n * `repos` is in position order and must include the primary (`repoFullName`).\n */\nexport function createEnvironmentSetupHook(\n run: SessionEnvironmentSetupRun,\n sessionId: string,\n repoFullName: string,\n environment: { repos: SessionEnvironmentRepository[]; workspaceSetupCommand?: string },\n): SandboxStartHook {\n const workspaceCommand = environment.workspaceSetupCommand?.trim() ? environment.workspaceSetupCommand : undefined;\n const workspaceMarker = workspaceCommand ? setupMarkerContent(workspaceCommand) : undefined;\n return async ({ sandbox }) => {\n if (!sandbox.executeCommand) {\n throw new Error(`Sandbox '${sandbox.id}' cannot run the session setup: no executeCommand implementation`);\n }\n const workdir = await resolveSessionWorkdir(sessionId, sandbox, repoFullName);\n const root = path.posix.dirname(workdir);\n const repos: SessionEnvironmentRepositoryGate[] = [];\n for (const repo of environment.repos) {\n const repoDir = repositoryDirectoryName(repo.slug);\n const marker = repo.setupCommand?.trim() ? setupMarkerContent(repo.setupCommand) : undefined;\n const setupDone = marker\n ? await timedPhase(`workspace.setup-marker(${repoDir})`, () =>\n repoMarkerMatches(sandbox, root, repoDir, marker),\n )\n : true;\n if (marker) {\n process.stderr.write(\n `[factory:setup] ${repo.slug}: ${setupDone ? 'marker matches, skipping setup command' : 'no matching marker, setup command will run'} (${root}/${repoSetupMarkerPath(repoDir)})\\n`,\n );\n }\n repos.push({\n ...repo,\n dir: `${root}/${repoDir}`,\n gate: {\n setupDone,\n markSetupDone: () => (marker ? writeRepoMarker(sandbox, root, repoDir, marker) : Promise.resolve()),\n },\n });\n }\n const workspaceDone = workspaceMarker\n ? await timedPhase('workspace.setup-marker(workspace)', () =>\n workspaceMarkerMatches(sandbox, root, workspaceMarker),\n )\n : true;\n await run(sandbox, {\n root,\n repos,\n workspace: {\n setupDone: workspaceDone,\n markSetupDone: () =>\n workspaceMarker ? writeWorkspaceMarker(sandbox, root, workspaceMarker) : Promise.resolve(),\n },\n });\n };\n}\n"],"mappings":"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"}
|