@mastra/factory 0.6.0-alpha.8 → 0.6.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +339 -0
  2. package/dist/auth.d.ts.map +1 -1
  3. package/dist/auth.js +9 -0
  4. package/dist/auth.js.map +1 -1
  5. package/dist/factory.d.ts.map +1 -1
  6. package/dist/factory.js +6 -0
  7. package/dist/factory.js.map +1 -1
  8. package/dist/integrations/github/routes.js +11 -2
  9. package/dist/integrations/github/routes.js.map +1 -1
  10. package/dist/integrations/github/sandbox.d.ts +11 -9
  11. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  12. package/dist/integrations/github/sandbox.js +8 -1
  13. package/dist/integrations/github/sandbox.js.map +1 -1
  14. package/dist/integrations/slack/slack.d.ts.map +1 -1
  15. package/dist/integrations/slack/slack.js +6 -1
  16. package/dist/integrations/slack/slack.js.map +1 -1
  17. package/dist/rules/dispatcher.d.ts.map +1 -1
  18. package/dist/rules/dispatcher.js +5 -5
  19. package/dist/rules/dispatcher.js.map +1 -1
  20. package/dist/rules/start-coordinator.js +2 -2
  21. package/dist/rules/start-coordinator.js.map +1 -1
  22. package/dist/sandbox/fleet.d.ts.map +1 -1
  23. package/dist/sandbox/fleet.js +3 -2
  24. package/dist/sandbox/fleet.js.map +1 -1
  25. package/dist/session/checkpoint-capture.d.ts +17 -0
  26. package/dist/session/checkpoint-capture.d.ts.map +1 -0
  27. package/dist/session/checkpoint-capture.js +27 -0
  28. package/dist/session/checkpoint-capture.js.map +1 -0
  29. package/dist/session/factory-session.d.ts.map +1 -1
  30. package/dist/session/factory-session.js +29 -21
  31. package/dist/session/factory-session.js.map +1 -1
  32. package/dist/session/filesystem-capture.d.ts +1 -1
  33. package/dist/session/filesystem-capture.d.ts.map +1 -1
  34. package/dist/session/filesystem-capture.js +1 -1
  35. package/dist/session/filesystem-capture.js.map +1 -1
  36. package/dist/session/first-exec-capture.d.ts +31 -0
  37. package/dist/session/first-exec-capture.d.ts.map +1 -0
  38. package/dist/session/first-exec-capture.js +31 -0
  39. package/dist/session/first-exec-capture.js.map +1 -0
  40. package/dist/session/first-message-capture.d.ts +29 -0
  41. package/dist/session/first-message-capture.d.ts.map +1 -0
  42. package/dist/session/first-message-capture.js +29 -0
  43. package/dist/session/first-message-capture.js.map +1 -0
  44. package/dist/storage/domains/source-control/base.d.ts +23 -0
  45. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  46. package/dist/storage/domains/source-control/base.js +29 -0
  47. package/dist/storage/domains/source-control/base.js.map +1 -1
  48. package/dist/storage/domains/source-control/inmemory.d.ts +6 -0
  49. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  50. package/dist/storage/domains/source-control/inmemory.js +16 -0
  51. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  52. package/dist/storage/domains/work-items/metrics.d.ts +32 -41
  53. package/dist/storage/domains/work-items/metrics.d.ts.map +1 -1
  54. package/dist/storage/domains/work-items/metrics.js +58 -83
  55. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  56. package/dist/workspace.d.ts +2 -2
  57. package/dist/workspace.d.ts.map +1 -1
  58. package/dist/workspace.js +1 -1
  59. package/dist/workspace.js.map +1 -1
  60. package/factory-skills/factory-review/SKILL.md +1 -1
  61. package/package.json +8 -8
@@ -1 +1 @@
1
- {"version":3,"file":"dispatcher.js","names":["#controller","#transitionService","#storage","#ownerId","#reconcileToolResults","#prepareBinding","#primeCredentials","#maxInFlight","#inFlight","#timer","#tick","#activeClaim","#claimAndStart","#track","#dispatchDecision","#dispatchPendingStart","#withLease","#executeDecision","#requireItem","#findBinding","#requireSession","#upsertLinkedItem","#requireOrPrepareBinding","#switchThread","#requireBinding"],"sources":["../../src/rules/dispatcher.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController, AgentControllerEventListener } from '@mastra/core/agent-controller';\nimport { RequestContext } from '@mastra/core/request-context';\n\nimport { resolveSkillInvocation } from '../skills/service.js';\nimport type { SkillSession } from '../skills/service.js';\nimport type {\n FactoryDeferredDecisionRecord,\n FactoryPendingStartRecord,\n FactoryRunBindingRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport type { FactoryTransitionService } from './transition-service.js';\nimport type { FactoryCommitDecision, FactoryRuleActor, FactoryRuleCausalEntry } from './types.js';\nimport { FACTORY_RULE_STAGES } from './types.js';\nimport { MAX_FACTORY_RULE_CAUSAL_DEPTH, validateFactoryRuleDecision } from './validation.js';\n\nconst LEASE_MS = 30_000;\nconst POLL_MS = 1_000;\nconst BATCH_SIZE = 10;\nconst MAX_ATTEMPTS = 5;\nconst MAX_ERROR_LENGTH = 512;\nconst MAX_BACKOFF_MS = 60_000;\nconst SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS = 10 * 60_000;\n// Dispatches can legitimately run for minutes. Woken skill invocations hold\n// capacity until their agent run reaches a terminal state; binding preparation\n// also runs detached from the poll loop under this concurrency cap.\nconst MAX_IN_FLIGHT = 25;\n\nfunction waitForAgentEndOrTimeout(agentEnd: Promise<void>): Promise<boolean> {\n return new Promise(resolve => {\n const timeout = setTimeout(() => resolve(false), SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS);\n timeout.unref?.();\n void agentEnd.then(() => {\n clearTimeout(timeout);\n resolve(true);\n });\n });\n}\n\ninterface DispatcherSession extends SkillSession {\n thread: {\n switch(input: { threadId: string }): Promise<unknown>;\n listActiveMessages(): Promise<Array<{ id: string }>>;\n };\n sendSignal(\n input: { id: string; type: 'user'; tagName: 'user'; contents: string },\n options: { requestContext: RequestContext; requireDelivery?: boolean },\n ): { accepted: Promise<{ accepted: true; runId?: string; action?: string }> };\n subscribe(listener: AgentControllerEventListener): () => void;\n}\n\ntype FactoryController = Pick<AgentController<MastraCodeState>, 'getSessionByResource'>;\n\nexport interface FactoryBindingPreparationInput {\n record: FactoryDeferredDecisionRecord;\n item: WorkItemRow;\n role: string;\n}\n\nexport interface FactoryDecisionDispatcherOptions {\n controller: FactoryController;\n transitionService: Pick<FactoryTransitionService, 'transition'>;\n storage: WorkItemsStorage;\n ownerId?: string;\n reconcileToolResults?: () => Promise<void>;\n prepareBinding?: (input: FactoryBindingPreparationInput) => Promise<void>;\n primeCredentials?: (tenant: { orgId: string; userId: string }) => Promise<void>;\n maxInFlight?: number;\n}\n\nfunction sanitizeDispatchError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n return message\n .replace(/\\b(?:bearer|token|api[-_ ]?key|authorization)\\s*[:=]?\\s*[^\\s,;]+/gi, '[redacted]')\n .slice(0, MAX_ERROR_LENGTH);\n}\n\nfunction retryAt(now: Date, attempts: number): Date {\n return new Date(now.getTime() + Math.min(1_000 * 2 ** Math.max(0, attempts - 1), MAX_BACKOFF_MS));\n}\n\nfunction externalSourceForDecision(decision: Extract<FactoryCommitDecision, { type: 'upsertLinkedWorkItem' }>) {\n const [integrationId, type] =\n decision.source === 'github-pr'\n ? ['github', 'pull-request']\n : decision.source === 'github-issue'\n ? ['github', 'issue']\n : decision.source === 'linear-issue'\n ? ['linear', 'issue']\n : ['factory', 'manual'];\n return { integrationId, type, externalId: decision.sourceKey, url: decision.url ?? undefined };\n}\n\nfunction deferredActor(record: FactoryDeferredDecisionRecord): FactoryRuleActor {\n const actor = record.actor;\n if (\n actor?.type === 'github' &&\n typeof actor.login === 'string' &&\n typeof actor.trusted === 'boolean' &&\n typeof actor.factoryAuthored === 'boolean'\n ) {\n return {\n type: 'github',\n login: actor.login,\n trusted: actor.trusted,\n factoryAuthored: actor.factoryAuthored,\n };\n }\n return { type: 'system', id: 'factory-rule-dispatcher' };\n}\n\nfunction leaseIdentity(\n record: Pick<FactoryDeferredDecisionRecord | FactoryPendingStartRecord, 'id' | 'orgId' | 'factoryProjectId'>,\n ownerId: string,\n) {\n return { id: record.id, orgId: record.orgId, factoryProjectId: record.factoryProjectId, ownerId };\n}\n\nasync function awaitNotification(\n result: Awaited<ReturnType<SkillSession['sendNotificationSignal']>>,\n requireDelivery = false,\n): Promise<void> {\n await result.persisted;\n if (!result.accepted) {\n if (requireDelivery) throw new Error('Factory notification was persisted without agent delivery.');\n return;\n }\n const accepted = await result.accepted;\n if (!requireDelivery) return;\n if (accepted.action === 'wake') {\n await accepted.output.consumeStream();\n return;\n }\n if (accepted.action !== 'deliver') {\n throw new Error(`Factory notification did not reach the agent (${String(accepted.action)}).`);\n }\n}\n\nexport class FactoryDecisionDispatcher {\n readonly #controller: FactoryController;\n readonly #transitionService: Pick<FactoryTransitionService, 'transition'>;\n readonly #storage: WorkItemsStorage;\n readonly #ownerId: string;\n readonly #reconcileToolResults?: () => Promise<void>;\n readonly #prepareBinding?: (input: FactoryBindingPreparationInput) => Promise<void>;\n readonly #primeCredentials?: (tenant: { orgId: string; userId: string }) => Promise<void>;\n readonly #maxInFlight: number;\n #timer?: ReturnType<typeof setInterval>;\n #activeClaim?: Promise<void>;\n readonly #inFlight = new Set<Promise<void>>();\n\n constructor(options: FactoryDecisionDispatcherOptions) {\n this.#controller = options.controller;\n this.#transitionService = options.transitionService;\n this.#storage = options.storage;\n this.#ownerId = options.ownerId ?? `factory-dispatcher:${randomUUID()}`;\n this.#reconcileToolResults = options.reconcileToolResults;\n this.#prepareBinding = options.prepareBinding;\n this.#primeCredentials = options.primeCredentials;\n const maxInFlight = options.maxInFlight ?? MAX_IN_FLIGHT;\n this.#maxInFlight = Number.isFinite(maxInFlight) && maxInFlight > 0 ? Math.floor(maxInFlight) : MAX_IN_FLIGHT;\n }\n\n start(): void {\n if (this.#timer) return;\n void this.#tick();\n this.#timer = setInterval(() => void this.#tick(), POLL_MS);\n this.#timer.unref?.();\n }\n\n async stop(): Promise<void> {\n if (this.#timer) clearInterval(this.#timer);\n this.#timer = undefined;\n await this.#activeClaim;\n await Promise.allSettled([...this.#inFlight]);\n }\n\n async runOnce(now = new Date()): Promise<void> {\n await Promise.all(await this.#claimAndStart(now));\n }\n\n /**\n * Claims a batch and starts dispatches without awaiting their completion.\n * Dispatches can legitimately take minutes (skill kickoffs consume the\n * agent's run stream; binding preparation provisions sandboxes), so awaiting\n * them here would freeze the poll loop and starve every other queued\n * decision. In-flight records stay protected from re-claim by lease renewal.\n */\n async #claimAndStart(now: Date): Promise<Array<Promise<void>>> {\n await this.#reconcileToolResults?.();\n const capacity = this.#maxInFlight - this.#inFlight.size;\n if (capacity <= 0) return [];\n const limit = Math.min(BATCH_SIZE, capacity);\n const leaseExpiresAt = new Date(now.getTime() + LEASE_MS);\n const decisions = await this.#storage.claimDeferredDecisions({\n ownerId: this.#ownerId,\n now,\n leaseExpiresAt,\n limit,\n });\n const startsLimit = limit - decisions.length;\n const starts =\n startsLimit > 0\n ? await this.#storage.claimPendingStarts({\n ownerId: this.#ownerId,\n now,\n leaseExpiresAt,\n limit: startsLimit,\n })\n : [];\n return [\n ...decisions.map(decision => this.#track(this.#dispatchDecision(decision, now))),\n ...starts.map(start => this.#track(this.#dispatchPendingStart(start, now))),\n ];\n }\n\n #track(dispatch: Promise<void>): Promise<void> {\n this.#inFlight.add(dispatch);\n void dispatch.catch(() => {}).then(() => this.#inFlight.delete(dispatch));\n return dispatch;\n }\n\n async #tick(): Promise<void> {\n if (this.#activeClaim) return;\n this.#activeClaim = this.#claimAndStart(new Date()).then(\n dispatches => {\n for (const dispatch of dispatches) {\n dispatch.catch(error => {\n console.error('Factory decision dispatch failed', sanitizeDispatchError(error));\n });\n }\n },\n error => {\n console.error('Factory decision dispatch cycle failed', sanitizeDispatchError(error));\n },\n );\n try {\n await this.#activeClaim;\n } finally {\n this.#activeClaim = undefined;\n }\n }\n\n async #dispatchDecision(record: FactoryDeferredDecisionRecord, now: Date): Promise<void> {\n try {\n const decision = validateFactoryRuleDecision(record.decision, record.causalChain.length);\n if (decision.type === 'reject') throw new Error('Deferred Factory decisions cannot reject.');\n await this.#withLease(\n async leaseExpiresAt =>\n this.#storage.renewDeferredDecisionLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt),\n async () => this.#executeDecision(record, decision),\n );\n const completed = await this.#storage.completeDeferredDecision(leaseIdentity(record, this.#ownerId), new Date());\n if (!completed) throw new Error('Factory decision lease was lost before completion.');\n } catch (error) {\n const terminal = record.attempts >= MAX_ATTEMPTS;\n await this.#storage.failDeferredDecision({\n ...leaseIdentity(record, this.#ownerId),\n now: new Date(),\n availableAt: retryAt(now, record.attempts),\n lastError: sanitizeDispatchError(error),\n terminal,\n });\n }\n }\n\n async #executeDecision(record: FactoryDeferredDecisionRecord, decision: FactoryCommitDecision): Promise<void> {\n const nextChain: FactoryRuleCausalEntry[] = [\n ...(record.causalChain as FactoryRuleCausalEntry[]),\n { ingressId: record.idempotencyKey, decisionType: decision.type },\n ];\n if (nextChain.length > MAX_FACTORY_RULE_CAUSAL_DEPTH) throw new Error('Factory rule causal depth exceeded.');\n\n switch (decision.type) {\n case 'transition': {\n const item = await this.#requireItem(record);\n const result = await this.#transitionService.transition({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: item.id,\n board: decision.board,\n stage: decision.stage,\n expectedRevision: item.revision,\n actor: { type: 'system', id: 'factory-rule-dispatcher' },\n ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}` },\n cause: 'rule_decision',\n causalChain: nextChain,\n });\n if (result.status === 'rejected') throw new Error(`${result.code}: ${result.reason}`);\n if (!decision.message) return;\n // Best-effort recipient lookup: no active binding (or no authenticated\n // session owner) means nobody is engaged with this item, so the\n // transition itself is the whole effect. A retry after a delivery\n // failure is safe because the transition replays by ingress identity.\n const binding = await this.#findBinding(record, decision.message.role);\n if (!binding) return;\n const startedBy = item.sessions[binding.role]?.startedBy;\n if (!startedBy) return;\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const session = await this.#requireSession(binding);\n await awaitNotification(\n await session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'rule-message',\n summary: decision.message.text,\n priority: 'high',\n payload: { message: decision.message.text },\n sourceId: record.id,\n dedupeKey: record.idempotencyKey,\n },\n {\n ifActive: { behavior: 'deliver' },\n ifIdle: { behavior: 'wake' },\n requestContext,\n },\n ),\n true,\n );\n return;\n }\n case 'upsertLinkedWorkItem': {\n await this.#upsertLinkedItem(record, decision, nextChain);\n return;\n }\n case 'invokeSkill': {\n const binding = await this.#requireOrPrepareBinding(record, decision.role);\n const item = record.workItemId ? await this.#storage.get({ orgId: record.orgId, id: record.workItemId }) : null;\n const startedBy = item?.sessions[binding.role]?.startedBy;\n if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const resolved = await resolveSkillInvocation(this.#controller, {\n resourceId: binding.resourceId,\n name: decision.skillName,\n arguments: decision.arguments,\n });\n const session = resolved.session as DispatcherSession;\n await this.#switchThread(session, binding);\n const delivered = await session.thread.listActiveMessages();\n if (delivered.some(message => message.id === record.id)) return;\n if (decision.precedingMessage) {\n await awaitNotification(\n await session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'stage-transition',\n summary: decision.precedingMessage,\n priority: 'medium',\n payload: { message: decision.precedingMessage },\n sourceId: `${record.id}:stage-transition`,\n dedupeKey: `${record.idempotencyKey}:stage-transition`,\n },\n {\n ifActive: { behavior: 'deliver' },\n ifIdle: { behavior: 'persist' },\n requestContext,\n },\n ),\n );\n }\n let resolveAgentEnd!: () => void;\n const agentEnd = new Promise<void>(resolve => {\n resolveAgentEnd = resolve;\n });\n const unsubscribe = session.subscribe(event => {\n if (event.type === 'agent_end') {\n resolveAgentEnd();\n }\n });\n\n try {\n const result = session.sendSignal(\n {\n id: record.id,\n type: 'user',\n tagName: 'user',\n contents: resolved.message,\n },\n // Without `requireDelivery` the session resolves `accepted` on the\n // next tick and swallows wake failures, so a kickoff that never\n // reached the agent would be marked succeeded and the thread would\n // stay empty forever.\n { requestContext, requireDelivery: true },\n );\n const settled = await result.accepted;\n if (settled.action !== 'wake' && settled.action !== 'deliver') {\n // An undefined action means the session did not verify delivery at\n // all — with `requireDelivery` set that is a contract violation, not\n // a success.\n throw new Error(`Factory skill invocation signal did not reach the agent (${String(settled.action)}).`);\n }\n if (settled.action === 'wake') {\n const observed = await waitForAgentEndOrTimeout(agentEnd);\n if (!observed) {\n console.warn('Factory skill run terminal event was not observed before timeout', {\n decisionId: record.id,\n runId: settled.runId,\n });\n }\n }\n } finally {\n unsubscribe();\n }\n return;\n }\n case 'sendMessage': {\n const binding = decision.prepareBinding\n ? await this.#requireOrPrepareBinding(record, decision.role)\n : await this.#requireBinding(record, decision.role);\n const item = record.workItemId ? await this.#storage.get({ orgId: record.orgId, id: record.workItemId }) : null;\n const startedBy = item?.sessions[binding.role]?.startedBy;\n if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const session = await this.#requireSession(binding);\n await awaitNotification(\n await session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'rule-message',\n summary: decision.message,\n priority: decision.priority ?? 'high',\n payload: { message: decision.message },\n sourceId: record.id,\n dedupeKey: record.idempotencyKey,\n },\n {\n ifActive: { behavior: 'deliver' },\n ifIdle: { behavior: decision.idleBehavior ?? 'wake' },\n requestContext,\n },\n ),\n true,\n );\n return;\n }\n case 'notify': {\n const binding = await this.#requireBinding(record);\n const session = await this.#requireSession(binding);\n await awaitNotification(\n await session.sendNotificationSignal({\n source: 'factory',\n kind: 'rule-notification',\n summary: decision.title,\n payload: { body: decision.body, level: decision.level },\n sourceId: record.id,\n dedupeKey: record.idempotencyKey,\n }),\n );\n }\n }\n }\n\n async #upsertLinkedItem(\n record: FactoryDeferredDecisionRecord,\n decision: Extract<FactoryCommitDecision, { type: 'upsertLinkedWorkItem' }>,\n causalChain: FactoryRuleCausalEntry[],\n ): Promise<void> {\n const result = await this.#storage.upsert({\n orgId: record.orgId,\n userId: 'factory-rule-dispatcher',\n factoryProjectId: record.factoryProjectId,\n input: {\n externalSource: externalSourceForDecision(decision),\n parentWorkItemId: record.workItemId,\n title: decision.title,\n stages: ['intake'],\n sessions: {},\n metadata: { ...decision.metadata, factoryRuleMaterializationKey: record.idempotencyKey },\n },\n reuseMode: 'preserve',\n });\n const materializedByDecision = result.item.metadata?.factoryRuleMaterializationKey === record.idempotencyKey;\n if (!materializedByDecision && (decision.stage === 'intake' || !result.item.stages.includes('intake'))) return;\n\n const board = decision.board;\n let expectedRevision = result.item.revision;\n if (materializedByDecision) {\n const initial = await this.#transitionService.transition({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: result.item.id,\n board,\n stage: 'intake',\n expectedRevision,\n actor: deferredActor(record),\n ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}:${result.item.id}:initial-entry` },\n cause: 'linked_item_materialized',\n causalChain,\n initialEntry: true,\n });\n if (initial.status === 'rejected') {\n if (result.created) await this.#storage.delete({ orgId: record.orgId, id: result.item.id });\n throw new Error(`${initial.code}: ${initial.reason}`);\n }\n expectedRevision = initial.revision;\n }\n if (decision.stage === 'intake') return;\n\n const moved = await this.#transitionService.transition({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: result.item.id,\n board,\n stage: decision.stage,\n expectedRevision,\n actor: { type: 'system', id: 'factory-rule-dispatcher' },\n ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}:${result.item.id}:destination` },\n cause: materializedByDecision ? 'linked_item_materialized' : 'linked_item_reconciled',\n causalChain,\n });\n if (moved.status === 'rejected') throw new Error(`${moved.code}: ${moved.reason}`);\n }\n\n async #requireItem(record: FactoryDeferredDecisionRecord) {\n if (!record.workItemId) throw new Error('Factory decision is not linked to a work item.');\n const item = await this.#storage.get({ orgId: record.orgId, id: record.workItemId });\n if (!item) throw new Error('Factory work item not found.');\n return item;\n }\n\n async #findBinding(\n record: FactoryDeferredDecisionRecord,\n role?: string,\n ): Promise<FactoryRunBindingRecord | undefined> {\n if (!record.workItemId) throw new Error('Factory decision is not linked to a work item.');\n const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId, record.workItemId);\n return bindings\n .filter(candidate => candidate.status === 'active' && (role === undefined || candidate.role === role))\n .sort((left, right) => {\n if (role === undefined && left.role === 'work' && right.role !== 'work') return -1;\n if (role === undefined && right.role === 'work' && left.role !== 'work') return 1;\n return right.createdAt.getTime() - left.createdAt.getTime() || left.id.localeCompare(right.id);\n })[0];\n }\n\n async #requireBinding(record: FactoryDeferredDecisionRecord, role?: string): Promise<FactoryRunBindingRecord> {\n const binding = await this.#findBinding(record, role);\n if (!binding) throw new Error(role ? `No active Factory binding for role ${role}.` : 'No active Factory binding.');\n return binding;\n }\n\n async #requireOrPrepareBinding(\n record: FactoryDeferredDecisionRecord,\n role: string,\n ): Promise<FactoryRunBindingRecord> {\n const binding = await this.#findBinding(record, role);\n if (binding) {\n const session = await this.#controller.getSessionByResource(binding.resourceId);\n if (session) return binding;\n }\n if (!this.#prepareBinding) {\n throw new Error(binding ? 'Bound Factory session not found.' : `No active Factory binding for role ${role}.`);\n }\n const item = await this.#requireItem(record);\n await this.#prepareBinding({ record, item, role });\n return this.#requireBinding(record, role);\n }\n\n async #requireSession(binding: FactoryRunBindingRecord): Promise<DispatcherSession> {\n const session = (await this.#controller.getSessionByResource(binding.resourceId)) as DispatcherSession | undefined;\n if (!session) throw new Error('Bound Factory session not found.');\n await this.#switchThread(session, binding);\n return session;\n }\n\n async #switchThread(session: SkillSession, binding: FactoryRunBindingRecord): Promise<void> {\n await (session as DispatcherSession).thread.switch({ threadId: binding.threadId });\n }\n\n async #withLease(\n renew: (leaseExpiresAt: Date) => Promise<unknown | null>,\n effect: () => Promise<void>,\n ): Promise<void> {\n let renewalFailure: unknown;\n let renewal = Promise.resolve();\n const timer = setInterval(\n () => {\n renewal = renewal.then(async () => {\n try {\n const renewed = await renew(new Date(Date.now() + LEASE_MS));\n if (!renewed) renewalFailure = new Error('Factory dispatch lease was lost during execution.');\n } catch (error) {\n renewalFailure = error;\n }\n });\n },\n Math.floor(LEASE_MS / 3),\n );\n timer.unref?.();\n try {\n await effect();\n await renewal;\n if (renewalFailure) throw renewalFailure;\n } finally {\n clearInterval(timer);\n await renewal;\n }\n }\n\n async #dispatchPendingStart(record: FactoryPendingStartRecord, now: Date): Promise<void> {\n try {\n await this.#withLease(\n async leaseExpiresAt =>\n this.#storage.renewPendingStartLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt),\n async () => {\n if (record.message === null) return;\n const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId);\n const binding = bindings.find(\n candidate => candidate.id === record.bindingId && candidate.status === 'active',\n );\n if (!binding) throw new Error('Prepared Factory binding is unavailable or revoked.');\n // Wake runs build the Factory workspace, which requires the\n // authenticated session owner on the request context.\n const item = await this.#storage.get({ orgId: record.orgId, id: binding.workItemId });\n const startedBy = item?.sessions[binding.role]?.startedBy;\n if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const session = await this.#requireSession(binding);\n await awaitNotification(\n await session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'run-kickoff',\n summary: record.message!,\n priority: 'high',\n payload: { message: record.message },\n sourceId: record.id,\n dedupeKey: `factory-kickoff:${record.kickoffKey}`,\n },\n { ifActive: { behavior: 'deliver' }, ifIdle: { behavior: 'wake' }, requestContext },\n ),\n true,\n );\n },\n );\n const completed = await this.#storage.completePendingStart(leaseIdentity(record, this.#ownerId), new Date());\n if (!completed) throw new Error('Factory kickoff lease was lost before completion.');\n } catch (error) {\n await this.#storage.failPendingStart({\n ...leaseIdentity(record, this.#ownerId),\n now: new Date(),\n availableAt: retryAt(now, record.attempts),\n lastError: sanitizeDispatchError(error),\n terminal: record.attempts >= MAX_ATTEMPTS,\n });\n }\n }\n}\n\nexport const FACTORY_DISPATCH_CONSTANTS = {\n leaseMs: LEASE_MS,\n pollMs: POLL_MS,\n batchSize: BATCH_SIZE,\n maxAttempts: MAX_ATTEMPTS,\n maxErrorLength: MAX_ERROR_LENGTH,\n maxBackoffMs: MAX_BACKOFF_MS,\n skillCompletionObservationTimeoutMs: SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS,\n maxInFlight: MAX_IN_FLIGHT,\n stages: FACTORY_RULE_STAGES,\n} as const;\n"],"mappings":"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"}
1
+ {"version":3,"file":"dispatcher.js","names":["#controller","#transitionService","#storage","#ownerId","#reconcileToolResults","#prepareBinding","#primeCredentials","#maxInFlight","#inFlight","#timer","#tick","#activeClaim","#claimAndStart","#track","#dispatchPendingStart","#dispatchDecision","#withLease","#executeDecision","#requireItem","#findBinding","#requireSession","#upsertLinkedItem","#requireOrPrepareBinding","#switchThread","#requireBinding"],"sources":["../../src/rules/dispatcher.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController, AgentControllerEventListener } from '@mastra/core/agent-controller';\nimport { RequestContext } from '@mastra/core/request-context';\n\nimport { resolveSkillInvocation } from '../skills/service.js';\nimport type { SkillSession } from '../skills/service.js';\nimport type {\n FactoryDeferredDecisionRecord,\n FactoryPendingStartRecord,\n FactoryRunBindingRecord,\n WorkItemRow,\n WorkItemsStorage,\n} from '../storage/domains/work-items/base.js';\nimport type { FactoryTransitionService } from './transition-service.js';\nimport type { FactoryCommitDecision, FactoryRuleActor, FactoryRuleCausalEntry } from './types.js';\nimport { FACTORY_RULE_STAGES } from './types.js';\nimport { MAX_FACTORY_RULE_CAUSAL_DEPTH, validateFactoryRuleDecision } from './validation.js';\n\nconst LEASE_MS = 30_000;\nconst POLL_MS = 1_000;\nconst BATCH_SIZE = 10;\nconst MAX_ATTEMPTS = 5;\nconst MAX_ERROR_LENGTH = 512;\nconst MAX_BACKOFF_MS = 60_000;\nconst SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS = 10 * 60_000;\n// Dispatches can legitimately run for minutes. Woken skill invocations hold\n// capacity until their agent run reaches a terminal state; binding preparation\n// also runs detached from the poll loop under this concurrency cap.\nconst MAX_IN_FLIGHT = 25;\n\nfunction waitForAgentEndOrTimeout(agentEnd: Promise<void>): Promise<boolean> {\n return new Promise(resolve => {\n const timeout = setTimeout(() => resolve(false), SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS);\n timeout.unref?.();\n void agentEnd.then(() => {\n clearTimeout(timeout);\n resolve(true);\n });\n });\n}\n\ninterface DispatcherSession extends SkillSession {\n thread: {\n switch(input: { threadId: string }): Promise<unknown>;\n listActiveMessages(): Promise<Array<{ id: string }>>;\n };\n sendSignal(\n input: { id: string; type: 'user'; tagName: 'user'; contents: string },\n options: { requestContext: RequestContext; requireDelivery?: boolean },\n ): { accepted: Promise<{ accepted: true; runId?: string; action?: string }> };\n subscribe(listener: AgentControllerEventListener): () => void;\n}\n\ntype FactoryController = Pick<AgentController<MastraCodeState>, 'getSessionByResource'>;\n\nexport interface FactoryBindingPreparationInput {\n record: FactoryDeferredDecisionRecord;\n item: WorkItemRow;\n role: string;\n}\n\nexport interface FactoryDecisionDispatcherOptions {\n controller: FactoryController;\n transitionService: Pick<FactoryTransitionService, 'transition'>;\n storage: WorkItemsStorage;\n ownerId?: string;\n reconcileToolResults?: () => Promise<void>;\n prepareBinding?: (input: FactoryBindingPreparationInput) => Promise<void>;\n primeCredentials?: (tenant: { orgId: string; userId: string }) => Promise<void>;\n maxInFlight?: number;\n}\n\nfunction sanitizeDispatchError(error: unknown): string {\n const message = error instanceof Error ? error.message : String(error);\n return message\n .replace(/\\b(?:bearer|token|api[-_ ]?key|authorization)\\s*[:=]?\\s*[^\\s,;]+/gi, '[redacted]')\n .slice(0, MAX_ERROR_LENGTH);\n}\n\nfunction retryAt(now: Date, attempts: number): Date {\n return new Date(now.getTime() + Math.min(1_000 * 2 ** Math.max(0, attempts - 1), MAX_BACKOFF_MS));\n}\n\nfunction externalSourceForDecision(decision: Extract<FactoryCommitDecision, { type: 'upsertLinkedWorkItem' }>) {\n const [integrationId, type] =\n decision.source === 'github-pr'\n ? ['github', 'pull-request']\n : decision.source === 'github-issue'\n ? ['github', 'issue']\n : decision.source === 'linear-issue'\n ? ['linear', 'issue']\n : ['factory', 'manual'];\n return { integrationId, type, externalId: decision.sourceKey, url: decision.url ?? undefined };\n}\n\nfunction deferredActor(record: FactoryDeferredDecisionRecord): FactoryRuleActor {\n const actor = record.actor;\n if (\n actor?.type === 'github' &&\n typeof actor.login === 'string' &&\n typeof actor.trusted === 'boolean' &&\n typeof actor.factoryAuthored === 'boolean'\n ) {\n return {\n type: 'github',\n login: actor.login,\n trusted: actor.trusted,\n factoryAuthored: actor.factoryAuthored,\n };\n }\n return { type: 'system', id: 'factory-rule-dispatcher' };\n}\n\nfunction leaseIdentity(\n record: Pick<FactoryDeferredDecisionRecord | FactoryPendingStartRecord, 'id' | 'orgId' | 'factoryProjectId'>,\n ownerId: string,\n) {\n return { id: record.id, orgId: record.orgId, factoryProjectId: record.factoryProjectId, ownerId };\n}\n\nasync function awaitNotification(\n result: Awaited<ReturnType<SkillSession['sendNotificationSignal']>>,\n requireDelivery = false,\n): Promise<void> {\n await result.persisted;\n if (!result.accepted) {\n if (requireDelivery) throw new Error('Factory notification was persisted without agent delivery.');\n return;\n }\n const accepted = await result.accepted;\n if (!requireDelivery) return;\n if (accepted.action === 'wake') {\n await accepted.output.consumeStream();\n return;\n }\n if (accepted.action !== 'deliver') {\n throw new Error(`Factory notification did not reach the agent (${String(accepted.action)}).`);\n }\n}\n\nexport class FactoryDecisionDispatcher {\n readonly #controller: FactoryController;\n readonly #transitionService: Pick<FactoryTransitionService, 'transition'>;\n readonly #storage: WorkItemsStorage;\n readonly #ownerId: string;\n readonly #reconcileToolResults?: () => Promise<void>;\n readonly #prepareBinding?: (input: FactoryBindingPreparationInput) => Promise<void>;\n readonly #primeCredentials?: (tenant: { orgId: string; userId: string }) => Promise<void>;\n readonly #maxInFlight: number;\n #timer?: ReturnType<typeof setInterval>;\n #activeClaim?: Promise<void>;\n readonly #inFlight = new Set<Promise<void>>();\n\n constructor(options: FactoryDecisionDispatcherOptions) {\n this.#controller = options.controller;\n this.#transitionService = options.transitionService;\n this.#storage = options.storage;\n this.#ownerId = options.ownerId ?? `factory-dispatcher:${randomUUID()}`;\n this.#reconcileToolResults = options.reconcileToolResults;\n this.#prepareBinding = options.prepareBinding;\n this.#primeCredentials = options.primeCredentials;\n const maxInFlight = options.maxInFlight ?? MAX_IN_FLIGHT;\n this.#maxInFlight = Number.isFinite(maxInFlight) && maxInFlight > 0 ? Math.floor(maxInFlight) : MAX_IN_FLIGHT;\n }\n\n start(): void {\n if (this.#timer) return;\n void this.#tick();\n this.#timer = setInterval(() => void this.#tick(), POLL_MS);\n this.#timer.unref?.();\n }\n\n async stop(): Promise<void> {\n if (this.#timer) clearInterval(this.#timer);\n this.#timer = undefined;\n await this.#activeClaim;\n await Promise.allSettled([...this.#inFlight]);\n }\n\n async runOnce(now = new Date()): Promise<void> {\n await Promise.all(await this.#claimAndStart(now));\n }\n\n /**\n * Claims a batch and starts dispatches without awaiting their completion.\n * Dispatches can legitimately take minutes (skill kickoffs consume the\n * agent's run stream; binding preparation provisions sandboxes), so awaiting\n * them here would freeze the poll loop and starve every other queued\n * decision. In-flight records stay protected from re-claim by lease renewal.\n */\n async #claimAndStart(now: Date): Promise<Array<Promise<void>>> {\n await this.#reconcileToolResults?.();\n const capacity = this.#maxInFlight - this.#inFlight.size;\n if (capacity <= 0) return [];\n const limit = Math.min(BATCH_SIZE, capacity);\n const leaseExpiresAt = new Date(now.getTime() + LEASE_MS);\n // Starts are claimed before deferred decisions: a pending start is a user\n // waiting on a brand-new session, while a deferred decision is a background\n // continuation of one that is already running. A deep decision queue must\n // never starve new sessions out of the tick.\n const starts = await this.#storage.claimPendingStarts({\n ownerId: this.#ownerId,\n now,\n leaseExpiresAt,\n limit,\n });\n const decisionsLimit = limit - starts.length;\n const decisions =\n decisionsLimit > 0\n ? await this.#storage.claimDeferredDecisions({\n ownerId: this.#ownerId,\n now,\n leaseExpiresAt,\n limit: decisionsLimit,\n })\n : [];\n return [\n ...starts.map(start => this.#track(this.#dispatchPendingStart(start, now))),\n ...decisions.map(decision => this.#track(this.#dispatchDecision(decision, now))),\n ];\n }\n\n #track(dispatch: Promise<void>): Promise<void> {\n this.#inFlight.add(dispatch);\n void dispatch.catch(() => {}).then(() => this.#inFlight.delete(dispatch));\n return dispatch;\n }\n\n async #tick(): Promise<void> {\n if (this.#activeClaim) return;\n this.#activeClaim = this.#claimAndStart(new Date()).then(\n dispatches => {\n for (const dispatch of dispatches) {\n dispatch.catch(error => {\n console.error('Factory decision dispatch failed', sanitizeDispatchError(error));\n });\n }\n },\n error => {\n console.error('Factory decision dispatch cycle failed', sanitizeDispatchError(error));\n },\n );\n try {\n await this.#activeClaim;\n } finally {\n this.#activeClaim = undefined;\n }\n }\n\n async #dispatchDecision(record: FactoryDeferredDecisionRecord, now: Date): Promise<void> {\n try {\n const decision = validateFactoryRuleDecision(record.decision, record.causalChain.length);\n if (decision.type === 'reject') throw new Error('Deferred Factory decisions cannot reject.');\n await this.#withLease(\n async leaseExpiresAt =>\n this.#storage.renewDeferredDecisionLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt),\n async () => this.#executeDecision(record, decision),\n );\n const completed = await this.#storage.completeDeferredDecision(leaseIdentity(record, this.#ownerId), new Date());\n if (!completed) throw new Error('Factory decision lease was lost before completion.');\n } catch (error) {\n const terminal = record.attempts >= MAX_ATTEMPTS;\n await this.#storage.failDeferredDecision({\n ...leaseIdentity(record, this.#ownerId),\n now: new Date(),\n availableAt: retryAt(now, record.attempts),\n lastError: sanitizeDispatchError(error),\n terminal,\n });\n }\n }\n\n async #executeDecision(record: FactoryDeferredDecisionRecord, decision: FactoryCommitDecision): Promise<void> {\n const nextChain: FactoryRuleCausalEntry[] = [\n ...(record.causalChain as FactoryRuleCausalEntry[]),\n { ingressId: record.idempotencyKey, decisionType: decision.type },\n ];\n if (nextChain.length > MAX_FACTORY_RULE_CAUSAL_DEPTH) throw new Error('Factory rule causal depth exceeded.');\n\n switch (decision.type) {\n case 'transition': {\n const item = await this.#requireItem(record);\n const result = await this.#transitionService.transition({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: item.id,\n board: decision.board,\n stage: decision.stage,\n expectedRevision: item.revision,\n actor: { type: 'system', id: 'factory-rule-dispatcher' },\n ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}` },\n cause: 'rule_decision',\n causalChain: nextChain,\n });\n if (result.status === 'rejected') throw new Error(`${result.code}: ${result.reason}`);\n if (!decision.message) return;\n // Best-effort recipient lookup: no active binding (or no authenticated\n // session owner) means nobody is engaged with this item, so the\n // transition itself is the whole effect. A retry after a delivery\n // failure is safe because the transition replays by ingress identity.\n const binding = await this.#findBinding(record, decision.message.role);\n if (!binding) return;\n const startedBy = item.sessions[binding.role]?.startedBy;\n if (!startedBy) return;\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const session = await this.#requireSession(binding);\n await awaitNotification(\n await session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'rule-message',\n summary: decision.message.text,\n priority: 'high',\n payload: { message: decision.message.text },\n sourceId: record.id,\n dedupeKey: record.idempotencyKey,\n },\n {\n ifActive: { behavior: 'deliver' },\n ifIdle: { behavior: 'wake' },\n requestContext,\n },\n ),\n true,\n );\n return;\n }\n case 'upsertLinkedWorkItem': {\n await this.#upsertLinkedItem(record, decision, nextChain);\n return;\n }\n case 'invokeSkill': {\n const binding = await this.#requireOrPrepareBinding(record, decision.role);\n const item = record.workItemId ? await this.#storage.get({ orgId: record.orgId, id: record.workItemId }) : null;\n const startedBy = item?.sessions[binding.role]?.startedBy;\n if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const resolved = await resolveSkillInvocation(this.#controller, {\n resourceId: binding.resourceId,\n name: decision.skillName,\n arguments: decision.arguments,\n });\n const session = resolved.session as DispatcherSession;\n await this.#switchThread(session, binding);\n const delivered = await session.thread.listActiveMessages();\n if (delivered.some(message => message.id === record.id)) return;\n if (decision.precedingMessage) {\n await awaitNotification(\n await session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'stage-transition',\n summary: decision.precedingMessage,\n priority: 'medium',\n payload: { message: decision.precedingMessage },\n sourceId: `${record.id}:stage-transition`,\n dedupeKey: `${record.idempotencyKey}:stage-transition`,\n },\n {\n ifActive: { behavior: 'deliver' },\n ifIdle: { behavior: 'persist' },\n requestContext,\n },\n ),\n );\n }\n let resolveAgentEnd!: () => void;\n const agentEnd = new Promise<void>(resolve => {\n resolveAgentEnd = resolve;\n });\n const unsubscribe = session.subscribe(event => {\n if (event.type === 'agent_end') {\n resolveAgentEnd();\n }\n });\n\n try {\n const result = session.sendSignal(\n {\n id: record.id,\n type: 'user',\n tagName: 'user',\n contents: resolved.message,\n },\n // Without `requireDelivery` the session resolves `accepted` on the\n // next tick and swallows wake failures, so a kickoff that never\n // reached the agent would be marked succeeded and the thread would\n // stay empty forever.\n { requestContext, requireDelivery: true },\n );\n const settled = await result.accepted;\n if (settled.action !== 'wake' && settled.action !== 'deliver') {\n // An undefined action means the session did not verify delivery at\n // all — with `requireDelivery` set that is a contract violation, not\n // a success.\n throw new Error(`Factory skill invocation signal did not reach the agent (${String(settled.action)}).`);\n }\n if (settled.action === 'wake') {\n const observed = await waitForAgentEndOrTimeout(agentEnd);\n if (!observed) {\n console.warn('Factory skill run terminal event was not observed before timeout', {\n decisionId: record.id,\n runId: settled.runId,\n });\n }\n }\n } finally {\n unsubscribe();\n }\n return;\n }\n case 'sendMessage': {\n const binding = decision.prepareBinding\n ? await this.#requireOrPrepareBinding(record, decision.role)\n : await this.#requireBinding(record, decision.role);\n const item = record.workItemId ? await this.#storage.get({ orgId: record.orgId, id: record.workItemId }) : null;\n const startedBy = item?.sessions[binding.role]?.startedBy;\n if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const session = await this.#requireSession(binding);\n await awaitNotification(\n await session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'rule-message',\n summary: decision.message,\n priority: decision.priority ?? 'high',\n payload: { message: decision.message },\n sourceId: record.id,\n dedupeKey: record.idempotencyKey,\n },\n {\n ifActive: { behavior: 'deliver' },\n ifIdle: { behavior: decision.idleBehavior ?? 'wake' },\n requestContext,\n },\n ),\n true,\n );\n return;\n }\n case 'notify': {\n const binding = await this.#requireBinding(record);\n const session = await this.#requireSession(binding);\n await awaitNotification(\n await session.sendNotificationSignal({\n source: 'factory',\n kind: 'rule-notification',\n summary: decision.title,\n payload: { body: decision.body, level: decision.level },\n sourceId: record.id,\n dedupeKey: record.idempotencyKey,\n }),\n );\n }\n }\n }\n\n async #upsertLinkedItem(\n record: FactoryDeferredDecisionRecord,\n decision: Extract<FactoryCommitDecision, { type: 'upsertLinkedWorkItem' }>,\n causalChain: FactoryRuleCausalEntry[],\n ): Promise<void> {\n const result = await this.#storage.upsert({\n orgId: record.orgId,\n userId: 'factory-rule-dispatcher',\n factoryProjectId: record.factoryProjectId,\n input: {\n externalSource: externalSourceForDecision(decision),\n parentWorkItemId: record.workItemId,\n title: decision.title,\n stages: ['intake'],\n sessions: {},\n metadata: { ...decision.metadata, factoryRuleMaterializationKey: record.idempotencyKey },\n },\n reuseMode: 'preserve',\n });\n const materializedByDecision = result.item.metadata?.factoryRuleMaterializationKey === record.idempotencyKey;\n if (!materializedByDecision && (decision.stage === 'intake' || !result.item.stages.includes('intake'))) return;\n\n const board = decision.board;\n let expectedRevision = result.item.revision;\n if (materializedByDecision) {\n const initial = await this.#transitionService.transition({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: result.item.id,\n board,\n stage: 'intake',\n expectedRevision,\n actor: deferredActor(record),\n ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}:${result.item.id}:initial-entry` },\n cause: 'linked_item_materialized',\n causalChain,\n initialEntry: true,\n });\n if (initial.status === 'rejected') {\n if (result.created) await this.#storage.delete({ orgId: record.orgId, id: result.item.id });\n throw new Error(`${initial.code}: ${initial.reason}`);\n }\n expectedRevision = initial.revision;\n }\n if (decision.stage === 'intake') return;\n\n const moved = await this.#transitionService.transition({\n orgId: record.orgId,\n factoryProjectId: record.factoryProjectId,\n workItemId: result.item.id,\n board,\n stage: decision.stage,\n expectedRevision,\n actor: { type: 'system', id: 'factory-rule-dispatcher' },\n ingress: { type: 'rule', identity: `decision:${record.idempotencyKey}:${result.item.id}:destination` },\n cause: materializedByDecision ? 'linked_item_materialized' : 'linked_item_reconciled',\n causalChain,\n });\n if (moved.status === 'rejected') throw new Error(`${moved.code}: ${moved.reason}`);\n }\n\n async #requireItem(record: FactoryDeferredDecisionRecord) {\n if (!record.workItemId) throw new Error('Factory decision is not linked to a work item.');\n const item = await this.#storage.get({ orgId: record.orgId, id: record.workItemId });\n if (!item) throw new Error('Factory work item not found.');\n return item;\n }\n\n async #findBinding(\n record: FactoryDeferredDecisionRecord,\n role?: string,\n ): Promise<FactoryRunBindingRecord | undefined> {\n if (!record.workItemId) throw new Error('Factory decision is not linked to a work item.');\n const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId, record.workItemId);\n return bindings\n .filter(candidate => candidate.status === 'active' && (role === undefined || candidate.role === role))\n .sort((left, right) => {\n if (role === undefined && left.role === 'work' && right.role !== 'work') return -1;\n if (role === undefined && right.role === 'work' && left.role !== 'work') return 1;\n return right.createdAt.getTime() - left.createdAt.getTime() || left.id.localeCompare(right.id);\n })[0];\n }\n\n async #requireBinding(record: FactoryDeferredDecisionRecord, role?: string): Promise<FactoryRunBindingRecord> {\n const binding = await this.#findBinding(record, role);\n if (!binding) throw new Error(role ? `No active Factory binding for role ${role}.` : 'No active Factory binding.');\n return binding;\n }\n\n async #requireOrPrepareBinding(\n record: FactoryDeferredDecisionRecord,\n role: string,\n ): Promise<FactoryRunBindingRecord> {\n const binding = await this.#findBinding(record, role);\n if (binding) {\n const session = await this.#controller.getSessionByResource(binding.resourceId);\n if (session) return binding;\n }\n if (!this.#prepareBinding) {\n throw new Error(binding ? 'Bound Factory session not found.' : `No active Factory binding for role ${role}.`);\n }\n const item = await this.#requireItem(record);\n await this.#prepareBinding({ record, item, role });\n return this.#requireBinding(record, role);\n }\n\n async #requireSession(binding: FactoryRunBindingRecord): Promise<DispatcherSession> {\n const session = (await this.#controller.getSessionByResource(binding.resourceId)) as DispatcherSession | undefined;\n if (!session) throw new Error('Bound Factory session not found.');\n await this.#switchThread(session, binding);\n return session;\n }\n\n async #switchThread(session: SkillSession, binding: FactoryRunBindingRecord): Promise<void> {\n await (session as DispatcherSession).thread.switch({ threadId: binding.threadId });\n }\n\n async #withLease(\n renew: (leaseExpiresAt: Date) => Promise<unknown | null>,\n effect: () => Promise<void>,\n ): Promise<void> {\n let renewalFailure: unknown;\n let renewal = Promise.resolve();\n const timer = setInterval(\n () => {\n renewal = renewal.then(async () => {\n try {\n const renewed = await renew(new Date(Date.now() + LEASE_MS));\n if (!renewed) renewalFailure = new Error('Factory dispatch lease was lost during execution.');\n } catch (error) {\n renewalFailure = error;\n }\n });\n },\n Math.floor(LEASE_MS / 3),\n );\n timer.unref?.();\n try {\n await effect();\n await renewal;\n if (renewalFailure) throw renewalFailure;\n } finally {\n clearInterval(timer);\n await renewal;\n }\n }\n\n async #dispatchPendingStart(record: FactoryPendingStartRecord, now: Date): Promise<void> {\n try {\n await this.#withLease(\n async leaseExpiresAt =>\n this.#storage.renewPendingStartLease(leaseIdentity(record, this.#ownerId), leaseExpiresAt),\n async () => {\n if (record.message === null) return;\n const bindings = await this.#storage.listRunBindings(record.orgId, record.factoryProjectId);\n const binding = bindings.find(\n candidate => candidate.id === record.bindingId && candidate.status === 'active',\n );\n if (!binding) throw new Error('Prepared Factory binding is unavailable or revoked.');\n // Wake runs build the Factory workspace, which requires the\n // authenticated session owner on the request context.\n const item = await this.#storage.get({ orgId: record.orgId, id: binding.workItemId });\n const startedBy = item?.sessions[binding.role]?.startedBy;\n if (!startedBy) throw new Error(`Factory binding ${binding.id} has no authenticated session owner.`);\n await this.#primeCredentials?.({ orgId: record.orgId, userId: startedBy });\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: startedBy, organizationId: record.orgId });\n const session = await this.#requireSession(binding);\n await awaitNotification(\n await session.sendNotificationSignal(\n {\n source: 'factory',\n kind: 'run-kickoff',\n summary: record.message!,\n priority: 'high',\n payload: { message: record.message },\n sourceId: record.id,\n dedupeKey: `factory-kickoff:${record.kickoffKey}`,\n },\n { ifActive: { behavior: 'deliver' }, ifIdle: { behavior: 'wake' }, requestContext },\n ),\n true,\n );\n },\n );\n const completed = await this.#storage.completePendingStart(leaseIdentity(record, this.#ownerId), new Date());\n if (!completed) throw new Error('Factory kickoff lease was lost before completion.');\n } catch (error) {\n await this.#storage.failPendingStart({\n ...leaseIdentity(record, this.#ownerId),\n now: new Date(),\n availableAt: retryAt(now, record.attempts),\n lastError: sanitizeDispatchError(error),\n terminal: record.attempts >= MAX_ATTEMPTS,\n });\n }\n }\n}\n\nexport const FACTORY_DISPATCH_CONSTANTS = {\n leaseMs: LEASE_MS,\n pollMs: POLL_MS,\n batchSize: BATCH_SIZE,\n maxAttempts: MAX_ATTEMPTS,\n maxErrorLength: MAX_ERROR_LENGTH,\n maxBackoffMs: MAX_BACKOFF_MS,\n skillCompletionObservationTimeoutMs: SKILL_COMPLETION_OBSERVATION_TIMEOUT_MS,\n maxInFlight: MAX_IN_FLIGHT,\n stages: FACTORY_RULE_STAGES,\n} as const;\n"],"mappings":"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"}
@@ -16,7 +16,7 @@ function escapeSkillBoundary(value) {
16
16
  async function resolveKickoffMessage(session, invocation) {
17
17
  if (!invocation) return null;
18
18
  if (invocation.type === "prompt") return invocation.prompt;
19
- const skills = session.getWorkspace().skills;
19
+ const skills = session.getWorkspace()?.skills;
20
20
  await skills?.maybeRefresh();
21
21
  const skill = await skills?.get(invocation.skillName);
22
22
  if (!skill || skill["user-invocable"] === false) throw new Error(`Skill not found: ${invocation.skillName}.`);
@@ -70,7 +70,7 @@ var FactoryStartCoordinator = class {
70
70
  if (!this.#sourceControl) throw new Error("Factory source control storage is unavailable");
71
71
  const sourceSession = await resolveSourceSession(this.#sourceControl, request);
72
72
  const requestContext = request.requestContext ?? new RequestContext();
73
- if (!request.requestContext) requestContext.set("user", {
73
+ if (!requestContext.get("user")) requestContext.set("user", {
74
74
  workosId: request.userId,
75
75
  organizationId: request.orgId
76
76
  });
@@ -1 +1 @@
1
- {"version":3,"file":"start-coordinator.js","names":["#controller","#storage","#transitionService","#sourceControl","#memorySettings"],"sources":["../../src/rules/start-coordinator.ts"],"sourcesContent":["import type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController } from '@mastra/core/agent-controller';\nimport { RequestContext } from '@mastra/core/request-context';\nimport { formatSkillActivation } from '@mastra/core/workspace';\n\nimport { hydrateFactorySession } from '../session/factory-session.js';\nimport type { MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport type { SourceControlSession, SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport type { CreateWorkItemInput, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactoryTransitionService } from './transition-service.js';\nimport type { FactoryRuleStage, FactoryTransitionResult } from './types.js';\n\nexport interface FactoryStartRequest {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n sessionId: string;\n threadTitle: string;\n threadTags?: Record<string, string>;\n kickoffKey: string;\n invocation?: { type: 'prompt'; prompt: string } | { type: 'skill'; skillName: string; arguments: string };\n destinationStage: FactoryRuleStage;\n defaultModelId?: string;\n workItem: {\n id?: string;\n role: string;\n input: CreateWorkItemInput;\n };\n requestContext?: RequestContext;\n}\n\nexport class FactoryStartTransitionError extends Error {\n readonly result: Extract<FactoryTransitionResult, { status: 'rejected' }>;\n\n constructor(result: Extract<FactoryTransitionResult, { status: 'rejected' }>) {\n super(result.reason);\n this.name = 'FactoryStartTransitionError';\n this.result = result;\n }\n}\n\nexport interface FactoryStartPreparedResult {\n workItemId: string;\n bindingId: string;\n threadId: string;\n resourceId: string;\n sessionId: string;\n branch: string;\n revision: number;\n kickoffStatus: 'pending' | 'leased' | 'retry' | 'sent' | 'failed';\n replayed: boolean;\n}\n\ntype FactoryController = AgentController<MastraCodeState>;\ntype FactorySession = Awaited<ReturnType<FactoryController['createSession']>>;\n\nfunction escapeSkillBoundary(value: string): string {\n return value.replaceAll('</skill>', '&lt;/skill&gt;');\n}\n\nasync function resolveKickoffMessage(\n session: FactorySession,\n invocation: FactoryStartRequest['invocation'],\n): Promise<string | null> {\n if (!invocation) return null;\n if (invocation.type === 'prompt') return invocation.prompt;\n\n const skills = session.getWorkspace().skills;\n await skills?.maybeRefresh();\n const skill = await skills?.get(invocation.skillName);\n if (!skill || skill['user-invocable'] === false) {\n throw new Error(`Skill not found: ${invocation.skillName}.`);\n }\n const args = invocation.arguments.trim();\n const content = `${formatSkillActivation(skill)}${args ? `\\n\\nARGUMENTS: ${args}` : ''}`.trim();\n return `<skill name=\"${skill.name}\">\\n${escapeSkillBoundary(content)}\\n</skill>`;\n}\n\nasync function resolveSourceSession(\n storage: SourceControlStorageHandle,\n request: FactoryStartRequest,\n): Promise<SourceControlSession> {\n const session = await storage.sessions.getBySessionId(request.sessionId);\n if (!session || session.orgId !== request.orgId || session.userId !== request.userId) {\n throw new Error('Factory session not found');\n }\n const projectRepository = await storage.projectRepositories.get({\n orgId: request.orgId,\n id: session.projectRepositoryId,\n });\n if (!projectRepository) throw new Error('Factory session repository not found');\n const connection = await storage.connections.get({ orgId: request.orgId, id: projectRepository.connectionId });\n if (!connection || connection.factoryProjectId !== request.factoryProjectId) {\n throw new Error('Factory session does not belong to this project');\n }\n return session;\n}\n\nasync function configureThread(session: FactorySession, request: FactoryStartRequest): Promise<string> {\n const threadId = session.thread.requireId();\n await session.thread.rename({ title: request.threadTitle });\n const settings = { ...(request.threadTags ?? {}), factorySessionId: request.sessionId };\n await Promise.all(Object.entries(settings).map(([key, value]) => session.thread.setSetting({ key, value })));\n return threadId;\n}\n\nexport class FactoryStartCoordinator {\n readonly #controller: FactoryController;\n readonly #storage: WorkItemsStorage;\n readonly #transitionService?: Pick<FactoryTransitionService, 'transition'>;\n readonly #sourceControl?: SourceControlStorageHandle;\n readonly #memorySettings?: MemorySettingsStorage;\n\n constructor(\n controller: FactoryController,\n storage: WorkItemsStorage,\n transitionService?: Pick<FactoryTransitionService, 'transition'>,\n sourceControl?: SourceControlStorageHandle,\n memorySettings?: MemorySettingsStorage,\n ) {\n this.#controller = controller;\n this.#storage = storage;\n this.#transitionService = transitionService;\n this.#sourceControl = sourceControl;\n this.#memorySettings = memorySettings;\n }\n\n async prepare(request: FactoryStartRequest): Promise<FactoryStartPreparedResult> {\n const storage = this.#storage;\n if (!this.#sourceControl) throw new Error('Factory source control storage is unavailable');\n const sourceSession = await resolveSourceSession(this.#sourceControl, request);\n const requestContext = request.requestContext ?? new RequestContext();\n if (!request.requestContext) {\n requestContext.set('user', { workosId: request.userId, organizationId: request.orgId });\n }\n // Sessions kicked off against third-party content (a PR under review, or\n // any pull-request-sourced work item) get `untrustedCheckout` so the SDK\n // never ingests the checkout's AGENTS.md/CLAUDE.md into the system prompt\n // or reminders — those files are attacker-writable in a PR branch.\n const untrustedCheckout =\n request.workItem.input.externalSource?.type === 'pull-request' ||\n (request.invocation?.type === 'skill' && request.invocation.skillName === 'factory-review');\n // The trusted ref the SDK may serve project instruction files from on an\n // untrusted checkout (the PR's base branch). Prefer the session record's\n // base branch; fall back to the intake metadata captured from the PR.\n const metadataBaseBranch = request.workItem.input.metadata?.baseBranch;\n const baseRef =\n (sourceSession.baseBranch || undefined) ??\n (typeof metadataBaseBranch === 'string' && metadataBaseBranch ? metadataBaseBranch : undefined);\n const sessionTags = {\n factoryProjectId: request.factoryProjectId,\n projectRepositoryId: sourceSession.projectRepositoryId,\n };\n const session = await this.#controller.createSession({\n id: sourceSession.sessionId,\n ownerId: request.userId,\n resourceId: sourceSession.sessionId,\n threadId: sourceSession.sessionId,\n requestContext,\n tags: sessionTags,\n });\n // Bound-agent authority gates (the transition tool, the factory-phase\n // processor, workspace token selection) resolve the session address from\n // controller state. Seed it server-side — `tags` covers fresh creation,\n // the explicit setState covers get-or-create returning a session another\n // caller created without them — so autonomous runs never depend on a\n // browser connecting to populate the state. `untrustedCheckout` is a\n // boolean so it rides only on state (tags are string-valued).\n await session.state.set({\n ...sessionTags,\n ...(untrustedCheckout ? { untrustedCheckout: true, ...(baseRef ? { baseRef } : {}) } : {}),\n });\n await hydrateFactorySession(session, {\n orgId: request.orgId,\n userId: request.userId,\n defaultModelId: request.defaultModelId,\n memorySettings: this.#memorySettings,\n });\n const threadId = await configureThread(session, request);\n const kickoffMessage = await resolveKickoffMessage(session, request.invocation);\n const prepared = await storage.prepareRunStart({\n orgId: request.orgId,\n userId: request.userId,\n factoryProjectId: request.factoryProjectId,\n workItem: { id: request.workItem.id, input: request.workItem.input },\n role: request.workItem.role,\n session: { sessionId: sourceSession.sessionId, branch: sourceSession.branch, threadId },\n resourceId: sourceSession.sessionId,\n kickoffKey: request.kickoffKey,\n kickoffMessage,\n });\n await session.thread.setSetting({ key: 'factoryWorkItemId', value: prepared.item.id });\n\n let revision = prepared.item.revision;\n if (prepared.item.stages.length !== 1 || prepared.item.stages[0] !== request.destinationStage) {\n if (!this.#transitionService) throw new Error('Factory transition service is unavailable.');\n const transition = await this.#transitionService.transition({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: prepared.item.id,\n board: prepared.item.externalSource?.type === 'pull-request' ? 'review' : 'work',\n stage: request.destinationStage,\n expectedRevision: prepared.item.revision,\n actor: { type: 'human', id: request.userId },\n ingress: { type: 'human', identity: `start:${request.kickoffKey}:transition` },\n cause: 'run_start',\n });\n if (transition.status === 'rejected') {\n await storage.markPendingStart(prepared.binding.id, 'failed', transition.reason);\n throw new FactoryStartTransitionError(transition);\n }\n revision = transition.revision;\n }\n\n if (kickoffMessage === null) {\n await storage.markPendingStart(prepared.binding.id, 'sent');\n prepared.pendingStart.status = 'sent';\n }\n\n return {\n workItemId: prepared.item.id,\n bindingId: prepared.binding.id,\n threadId,\n resourceId: sourceSession.sessionId,\n sessionId: sourceSession.sessionId,\n branch: sourceSession.branch,\n revision,\n kickoffStatus: prepared.pendingStart.status,\n replayed: prepared.replayed,\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"start-coordinator.js","names":["#controller","#storage","#transitionService","#sourceControl","#memorySettings"],"sources":["../../src/rules/start-coordinator.ts"],"sourcesContent":["import type { MastraCodeState } from '@mastra/code-sdk/schema';\nimport type { AgentController } from '@mastra/core/agent-controller';\nimport { RequestContext } from '@mastra/core/request-context';\nimport { formatSkillActivation } from '@mastra/core/workspace';\n\nimport { hydrateFactorySession } from '../session/factory-session.js';\nimport type { MemorySettingsStorage } from '../storage/domains/memory-settings/base.js';\nimport type { SourceControlSession, SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport type { CreateWorkItemInput, WorkItemsStorage } from '../storage/domains/work-items/base.js';\nimport type { FactoryTransitionService } from './transition-service.js';\nimport type { FactoryRuleStage, FactoryTransitionResult } from './types.js';\n\nexport interface FactoryStartRequest {\n orgId: string;\n userId: string;\n factoryProjectId: string;\n sessionId: string;\n threadTitle: string;\n threadTags?: Record<string, string>;\n kickoffKey: string;\n invocation?: { type: 'prompt'; prompt: string } | { type: 'skill'; skillName: string; arguments: string };\n destinationStage: FactoryRuleStage;\n defaultModelId?: string;\n workItem: {\n id?: string;\n role: string;\n input: CreateWorkItemInput;\n };\n requestContext?: RequestContext;\n}\n\nexport class FactoryStartTransitionError extends Error {\n readonly result: Extract<FactoryTransitionResult, { status: 'rejected' }>;\n\n constructor(result: Extract<FactoryTransitionResult, { status: 'rejected' }>) {\n super(result.reason);\n this.name = 'FactoryStartTransitionError';\n this.result = result;\n }\n}\n\nexport interface FactoryStartPreparedResult {\n workItemId: string;\n bindingId: string;\n threadId: string;\n resourceId: string;\n sessionId: string;\n branch: string;\n revision: number;\n kickoffStatus: 'pending' | 'leased' | 'retry' | 'sent' | 'failed';\n replayed: boolean;\n}\n\ntype FactoryController = AgentController<MastraCodeState>;\ntype FactorySession = Awaited<ReturnType<FactoryController['createSession']>>;\n\nfunction escapeSkillBoundary(value: string): string {\n return value.replaceAll('</skill>', '&lt;/skill&gt;');\n}\n\nasync function resolveKickoffMessage(\n session: FactorySession,\n invocation: FactoryStartRequest['invocation'],\n): Promise<string | null> {\n if (!invocation) return null;\n if (invocation.type === 'prompt') return invocation.prompt;\n\n const skills = session.getWorkspace()?.skills;\n await skills?.maybeRefresh();\n const skill = await skills?.get(invocation.skillName);\n if (!skill || skill['user-invocable'] === false) {\n throw new Error(`Skill not found: ${invocation.skillName}.`);\n }\n const args = invocation.arguments.trim();\n const content = `${formatSkillActivation(skill)}${args ? `\\n\\nARGUMENTS: ${args}` : ''}`.trim();\n return `<skill name=\"${skill.name}\">\\n${escapeSkillBoundary(content)}\\n</skill>`;\n}\n\nasync function resolveSourceSession(\n storage: SourceControlStorageHandle,\n request: FactoryStartRequest,\n): Promise<SourceControlSession> {\n const session = await storage.sessions.getBySessionId(request.sessionId);\n if (!session || session.orgId !== request.orgId || session.userId !== request.userId) {\n throw new Error('Factory session not found');\n }\n const projectRepository = await storage.projectRepositories.get({\n orgId: request.orgId,\n id: session.projectRepositoryId,\n });\n if (!projectRepository) throw new Error('Factory session repository not found');\n const connection = await storage.connections.get({ orgId: request.orgId, id: projectRepository.connectionId });\n if (!connection || connection.factoryProjectId !== request.factoryProjectId) {\n throw new Error('Factory session does not belong to this project');\n }\n return session;\n}\n\nasync function configureThread(session: FactorySession, request: FactoryStartRequest): Promise<string> {\n const threadId = session.thread.requireId();\n await session.thread.rename({ title: request.threadTitle });\n const settings = { ...(request.threadTags ?? {}), factorySessionId: request.sessionId };\n await Promise.all(Object.entries(settings).map(([key, value]) => session.thread.setSetting({ key, value })));\n return threadId;\n}\n\nexport class FactoryStartCoordinator {\n readonly #controller: FactoryController;\n readonly #storage: WorkItemsStorage;\n readonly #transitionService?: Pick<FactoryTransitionService, 'transition'>;\n readonly #sourceControl?: SourceControlStorageHandle;\n readonly #memorySettings?: MemorySettingsStorage;\n\n constructor(\n controller: FactoryController,\n storage: WorkItemsStorage,\n transitionService?: Pick<FactoryTransitionService, 'transition'>,\n sourceControl?: SourceControlStorageHandle,\n memorySettings?: MemorySettingsStorage,\n ) {\n this.#controller = controller;\n this.#storage = storage;\n this.#transitionService = transitionService;\n this.#sourceControl = sourceControl;\n this.#memorySettings = memorySettings;\n }\n\n async prepare(request: FactoryStartRequest): Promise<FactoryStartPreparedResult> {\n const storage = this.#storage;\n if (!this.#sourceControl) throw new Error('Factory source control storage is unavailable');\n const sourceSession = await resolveSourceSession(this.#sourceControl, request);\n const requestContext = request.requestContext ?? new RequestContext();\n if (!requestContext.get('user')) {\n requestContext.set('user', { workosId: request.userId, organizationId: request.orgId });\n }\n // Sessions kicked off against third-party content (a PR under review, or\n // any pull-request-sourced work item) get `untrustedCheckout` so the SDK\n // never ingests the checkout's AGENTS.md/CLAUDE.md into the system prompt\n // or reminders — those files are attacker-writable in a PR branch.\n const untrustedCheckout =\n request.workItem.input.externalSource?.type === 'pull-request' ||\n (request.invocation?.type === 'skill' && request.invocation.skillName === 'factory-review');\n // The trusted ref the SDK may serve project instruction files from on an\n // untrusted checkout (the PR's base branch). Prefer the session record's\n // base branch; fall back to the intake metadata captured from the PR.\n const metadataBaseBranch = request.workItem.input.metadata?.baseBranch;\n const baseRef =\n (sourceSession.baseBranch || undefined) ??\n (typeof metadataBaseBranch === 'string' && metadataBaseBranch ? metadataBaseBranch : undefined);\n const sessionTags = {\n factoryProjectId: request.factoryProjectId,\n projectRepositoryId: sourceSession.projectRepositoryId,\n };\n const session = await this.#controller.createSession({\n id: sourceSession.sessionId,\n ownerId: request.userId,\n resourceId: sourceSession.sessionId,\n threadId: sourceSession.sessionId,\n requestContext,\n tags: sessionTags,\n });\n // Bound-agent authority gates (the transition tool, the factory-phase\n // processor, workspace token selection) resolve the session address from\n // controller state. Seed it server-side — `tags` covers fresh creation,\n // the explicit setState covers get-or-create returning a session another\n // caller created without them — so autonomous runs never depend on a\n // browser connecting to populate the state. `untrustedCheckout` is a\n // boolean so it rides only on state (tags are string-valued).\n await session.state.set({\n ...sessionTags,\n ...(untrustedCheckout ? { untrustedCheckout: true, ...(baseRef ? { baseRef } : {}) } : {}),\n });\n await hydrateFactorySession(session, {\n orgId: request.orgId,\n userId: request.userId,\n defaultModelId: request.defaultModelId,\n memorySettings: this.#memorySettings,\n });\n const threadId = await configureThread(session, request);\n const kickoffMessage = await resolveKickoffMessage(session, request.invocation);\n const prepared = await storage.prepareRunStart({\n orgId: request.orgId,\n userId: request.userId,\n factoryProjectId: request.factoryProjectId,\n workItem: { id: request.workItem.id, input: request.workItem.input },\n role: request.workItem.role,\n session: { sessionId: sourceSession.sessionId, branch: sourceSession.branch, threadId },\n resourceId: sourceSession.sessionId,\n kickoffKey: request.kickoffKey,\n kickoffMessage,\n });\n await session.thread.setSetting({ key: 'factoryWorkItemId', value: prepared.item.id });\n\n let revision = prepared.item.revision;\n if (prepared.item.stages.length !== 1 || prepared.item.stages[0] !== request.destinationStage) {\n if (!this.#transitionService) throw new Error('Factory transition service is unavailable.');\n const transition = await this.#transitionService.transition({\n orgId: request.orgId,\n factoryProjectId: request.factoryProjectId,\n workItemId: prepared.item.id,\n board: prepared.item.externalSource?.type === 'pull-request' ? 'review' : 'work',\n stage: request.destinationStage,\n expectedRevision: prepared.item.revision,\n actor: { type: 'human', id: request.userId },\n ingress: { type: 'human', identity: `start:${request.kickoffKey}:transition` },\n cause: 'run_start',\n });\n if (transition.status === 'rejected') {\n await storage.markPendingStart(prepared.binding.id, 'failed', transition.reason);\n throw new FactoryStartTransitionError(transition);\n }\n revision = transition.revision;\n }\n\n if (kickoffMessage === null) {\n await storage.markPendingStart(prepared.binding.id, 'sent');\n prepared.pendingStart.status = 'sent';\n }\n\n return {\n workItemId: prepared.item.id,\n bindingId: prepared.binding.id,\n threadId,\n resourceId: sourceSession.sessionId,\n sessionId: sourceSession.sessionId,\n branch: sourceSession.branch,\n revision,\n kickoffStatus: prepared.pendingStart.status,\n replayed: prepared.replayed,\n };\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/sandbox/fleet.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fleet.d.ts","sourceRoot":"","sources":["../../src/sandbox/fleet.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,4 @@
1
+ import { timedPhase } from "../timing.js";
1
2
  import path from "path";
2
3
  //#region src/sandbox/fleet.ts
3
4
  /**
@@ -259,7 +260,7 @@ var SandboxFleet = class {
259
260
  ...options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}
260
261
  });
261
262
  try {
262
- await reattached.start();
263
+ await timedPhase("sandbox.reattach", () => reattached.start());
263
264
  return reattached;
264
265
  } catch {
265
266
  await store.setSandboxId(null);
@@ -277,7 +278,7 @@ var SandboxFleet = class {
277
278
  ...env ? { env } : {},
278
279
  ...options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}
279
280
  });
280
- await sandbox.start();
281
+ await timedPhase("sandbox.provision", () => sandbox.start());
281
282
  this.#liveCount += 1;
282
283
  const providerSandboxId = await readProviderSandboxId(sandbox);
283
284
  if (providerSandboxId) await store.setSandboxId(providerSandboxId);
@@ -1 +1 @@
1
- {"version":3,"file":"fleet.js","names":["#config","#inflight","#liveCount","#factory","#ensureSandboxUncoalesced","#build"],"sources":["../../src/sandbox/fleet.ts"],"sourcesContent":["/**\n * Project sandbox fleet: provisioning, reattach, teardown, and budgeting.\n *\n * Server-hosted projects never run on the web host itself. Each project gets\n * its own isolated sandbox (a `WorkspaceSandbox`, e.g. a Railway VM) `clone()`d\n * from the machine the factory was configured with. This module owns everything\n * about that fleet — which provider is active, where checkouts live inside a\n * sandbox, the idle window, the per-replica budget, and the\n * provision/reattach/teardown lifecycle — but knows nothing about what runs\n * inside a sandbox (git materialization lives with its feature, e.g. the\n * GitHub integration's `sandbox.ts`).\n *\n * The fleet is constructed once at boot with the machine config (or none, when\n * sandboxes are disabled) and handed to consumers — no global registry.\n * Persistence of the provider's reattach id is delegated to the caller via\n * {@link SandboxBindingStore}, so the fleet stays storage-agnostic. Tests can\n * swap the low-level construction via {@link SandboxFleet.setFactory}.\n */\n\nimport path from 'node:path';\n\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\n/** Minimal command result shape sandbox consumers depend on. */\nexport interface SandboxCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Minimal live-sandbox surface fleet consumers need: an id, a way to start it,\n * a way to learn the provider's reattach id, and command execution.\n */\nexport interface MaterializationSandbox {\n readonly id: string;\n start(): Promise<void>;\n getInfo(): Promise<{ metadata?: Record<string, unknown> }>;\n executeCommand(\n command: string,\n args?: string[],\n options?: { timeout?: number; env?: Record<string, string | undefined> },\n ): Promise<SandboxCommandResult>;\n /** Update an environment variable for future commands in this sandbox. */\n setEnvironmentVariable?(name: string, value: string): void;\n /** Tear down the underlying VM. Optional: providers without it are no-ops. */\n stop?(): Promise<void>;\n}\n\n/** Options for building (or reattaching) one sandbox. */\nexport interface SandboxCreateOptions {\n /** Reattach to this existing provider VM instead of provisioning a new one. */\n providerSandboxId?: string;\n /**\n * Environment variables for commands run in the sandbox. Adapter-level\n * only: merged into every `executeCommand`, never baked into the provider\n * VM (see `SandboxFleet.#build`).\n */\n env?: Record<string, string>;\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n /** Idle teardown window (minutes). The provider stops the VM after this idle period. */\n idleTimeoutMinutes?: number;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n checkpointName?: string;\n}\n\n/**\n * A coarse-grained step of the sandbox-preparation flow, reported as it happens\n * so the UI can show the user what the server is doing instead of a static\n * \"Preparing…\" toast. `phase` is a stable machine token; `message` is\n * user-facing copy.\n */\nexport interface PrepareProgress {\n phase: 'reattaching' | 'provisioning' | 'preparing-workspace' | 'cloning' | 'pulling' | 'finalizing' | 'done';\n message: string;\n}\n\n/** Callback invoked with each preparation step. Best-effort; never throws. */\nexport type ProgressFn = (event: PrepareProgress) => void;\n\n/** Invoke a progress callback without letting it break the actual work. */\nexport function reportProgress(onProgress: ProgressFn | undefined, event: PrepareProgress): void {\n if (!onProgress) return;\n try {\n onProgress(event);\n } catch {\n // Progress reporting must never break the actual work.\n }\n}\n\n/**\n * Factory that builds a (not-yet-started) sandbox. When `providerSandboxId` is\n * provided the sandbox should reattach to that existing VM instead of\n * provisioning a new one.\n */\nexport type SandboxFactory = (opts: SandboxCreateOptions) => MaterializationSandbox;\n\n/** Raised when provisioning would exceed the per-replica sandbox budget. */\nexport class SandboxBudgetError extends Error {\n readonly code = 'sandbox-budget-exceeded' as const;\n constructor(readonly max: number) {\n super(\n `Sandbox budget exceeded: this server already has ${max} active sandbox(es), ` +\n `the configured per-replica maximum. Close an existing repository's sandbox and try again.`,\n );\n this.name = 'SandboxBudgetError';\n }\n}\n\n/** Optional knobs for provisioning/reattaching one sandbox. */\nexport interface EnsureSandboxOptions {\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n}\n\n/**\n * Where a feature persists its sandbox binding. The fleet reads the stored\n * reattach id and writes updates through this seam so it stays agnostic of\n * the owning table (GitHub projects today, anything else tomorrow).\n */\nexport interface SandboxBindingStore {\n /** Stored provider reattach id from a previous provisioning, if any. */\n readonly sandboxId: string | null;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n readonly checkpointName?: string;\n /** Persist a freshly provisioned provider id, or clear a stale one with `null`. */\n setSandboxId(id: string | null): Promise<void>;\n /** Clear all stored sandbox state (reattach id + materialization mark) on teardown. */\n clear(): Promise<void>;\n}\n\n/**\n * Stable identity for one binding's in-flight provision work, used to coalesce\n * concurrent `ensureSandbox` calls. Prefer `checkpointName` — it is a pure\n * function of the owning session and is set before the first provision, which\n * is exactly when the herd forms (the stored `sandboxId` is still null then).\n * Fall back to the stored provider id, and skip coalescing entirely for\n * bindings with neither: keying those on a shared constant would wrongly\n * funnel *different* bindings onto one sandbox.\n */\nfunction coalesceKey(store: SandboxBindingStore): string | undefined {\n if (store.checkpointName) return `checkpoint:${store.checkpointName}`;\n if (store.sandboxId) return `sandbox:${store.sandboxId}`;\n return undefined;\n}\n\n/**\n * Adapt a cloned `WorkspaceSandbox` to the minimal surface this module needs.\n * Lifecycle goes through the `_`-prefixed wrappers when present (they add\n * status tracking and concurrency safety on `MastraSandbox` subclasses),\n * falling back to the plain methods for interface-only implementations.\n */\nfunction toMaterializationSandbox(\n sandbox: WorkspaceSandbox,\n initialEnvironment: Record<string, string> = {},\n): MaterializationSandbox {\n if (typeof sandbox.executeCommand !== 'function') {\n throw new Error(\n `Sandbox provider '${sandbox.provider}' does not implement executeCommand() — cannot materialize repos.`,\n );\n }\n const lifecycle = sandbox as { _start?(): Promise<void>; _stop?(): Promise<void> };\n const environment = { ...initialEnvironment };\n return {\n id: sandbox.id,\n start: async () => {\n await (lifecycle._start ?? sandbox.start)?.call(sandbox);\n },\n getInfo: async () => (await sandbox.getInfo?.()) ?? {},\n executeCommand: (command, args, options) =>\n sandbox.executeCommand!(command, args, {\n ...options,\n env: { ...environment, ...options?.env },\n }),\n setEnvironmentVariable: (name, value) => {\n environment[name] = value;\n },\n stop: async () => {\n await (lifecycle._stop ?? sandbox.stop)?.call(sandbox);\n },\n };\n}\n\n/**\n * The provider's reattach id for a started sandbox. For Railway this is the\n * underlying `railwaySandboxId` in `getInfo().metadata`. Providers without a\n * provider-native id (e.g. local) reattach by construction id, so fall back\n * to the sandbox's own logical id.\n */\nasync function readProviderSandboxId(sandbox: MaterializationSandbox): Promise<string | undefined> {\n const info = await sandbox.getInfo();\n const id = info.metadata?.railwaySandboxId ?? info.metadata?.sandboxId;\n return typeof id === 'string' ? id : sandbox.id;\n}\n\n/** Keep each path piece a single safe segment (no separators or traversal). */\nfunction sanitizeSegment(segment: string): string {\n const cleaned = segment.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\\.+/, '');\n return cleaned || 'repo';\n}\n\n/** Resolve a workdir under `root`, refusing any path that escapes the configured root. */\nexport function resolveContainedLocalWorkdir(root: string, ...segments: string[]): string {\n const resolvedRoot = path.resolve(root);\n const resolved = path.resolve(resolvedRoot, ...segments);\n if (resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`)) return resolved;\n throw new Error(`Refusing to use local sandbox path outside configured root: ${resolved}`);\n}\n\n/**\n * Factory-resolved sandbox runtime the fleet is constructed with: the machine\n * projects clone their per-project sandboxes from, plus the knobs the factory\n * resolved around it.\n */\nexport interface SandboxFleetConfig {\n /**\n * Template machine (validated by the factory to implement `clone()`).\n * Never started — acts purely as the credential/default holder that\n * per-project sandboxes are cloned from.\n */\n machine: WorkspaceSandbox;\n /** In-sandbox base directory repos check out under (no trailing slash). */\n workdirBase: string;\n /** Per-replica cap on concurrently provisioned sandboxes. 0 = unlimited. */\n maxSandboxes?: number;\n}\n\n/**\n * The sandbox fleet for one deployment. Constructed once at boot — with a\n * config when a sandbox machine was configured, or without one when sandboxes\n * are disabled (every provisioning entry point then throws and\n * {@link enabled} reports `false` so features stay off).\n */\nexport class SandboxFleet {\n readonly #config: SandboxFleetConfig | undefined;\n #factory: SandboxFactory | undefined;\n #liveCount = 0;\n /** In-flight `ensureSandbox` work, keyed per binding so concurrent callers coalesce. */\n readonly #inflight = new Map<string, Promise<MaterializationSandbox>>();\n\n constructor(config?: SandboxFleetConfig) {\n this.#config = config;\n }\n\n /**\n * True when a sandbox machine was configured. The factory validates the\n * machine implements `clone()` at boot, so a configured fleet is usable —\n * sandbox-backed projects stay off only when the slot was omitted.\n */\n get enabled(): boolean {\n return this.#config !== undefined;\n }\n\n /**\n * Name of the active sandbox provider — the configured machine's `provider`\n * discriminator (`'railway'`, `'local'`, …), or `'none'` when the fleet was\n * constructed without a config. Diagnostic only; feature gating goes\n * through {@link enabled}.\n */\n get provider(): string {\n return this.#config?.machine.provider ?? 'none';\n }\n\n /**\n * Idle teardown window for provisioned sandboxes, in minutes; defaults to 30.\n * Read back from the machine's own config when it exposes one\n * (Railway's `idleTimeoutMinutes`) — the knob lives on the sandbox, the\n * fleet only needs it to schedule GC and stamp sandbox clones. Advisory:\n * providers without idle GC ignore it, and a re-open detects a torn-down VM\n * and re-provisions cleanly.\n */\n get idleMinutes(): number {\n const machine = this.#config?.machine as { idleTimeoutMinutes?: unknown } | undefined;\n const minutes = machine?.idleTimeoutMinutes;\n return typeof minutes === 'number' && Number.isFinite(minutes) && minutes > 0 ? minutes : 30;\n }\n\n /**\n * Per-replica cap on concurrently *provisioned* sandboxes. 0 means unlimited.\n * This is a lightweight per-process budget to keep a single replica from\n * exhausting provider quota — it is not a global, cross-replica scheduler\n * (that is a deferred follow-up).\n */\n get maxSandboxes(): number {\n return this.#config?.maxSandboxes ?? 0;\n }\n\n /**\n * Count of sandboxes this fleet has freshly provisioned and not yet torn\n * down. Reattaches to existing VMs do not count (they reuse an already-billed\n * sandbox). Used to enforce {@link maxSandboxes}.\n */\n get liveCount(): number {\n return this.#liveCount;\n }\n\n /** For tests: reset the live-sandbox counter to a known state. */\n __resetLiveCount(value = 0): void {\n this.#liveCount = value;\n }\n\n /** Override the sandbox factory (tests). */\n setFactory(factory: SandboxFactory): void {\n this.#factory = factory;\n }\n\n /** Reset to the default machine-cloning factory. */\n resetFactory(): void {\n this.#factory = undefined;\n }\n\n /**\n * Compute the in-sandbox working directory for a repo: a nested\n * `<base>/<owner>/<name>` layout under the factory-resolved checkout base.\n * Nesting keeps same-name repos apart (`acme/api` vs `other/api`) — cloud\n * sandboxes are one-per-project so it's merely tidy there, but local\n * checkouts share one host root where it prevents collisions. Server-side\n * only; never derived from client input.\n */\n computeWorkdir(repoFullName: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n const [owner, name] = repoFullName.split('/', 2);\n return `${this.#config.workdirBase}/${sanitizeSegment(owner || 'unknown')}/${sanitizeSegment(name || 'repo')}`;\n }\n\n /**\n * Compute the host working directory for a local GitHub session checkout.\n * This is server-derived only: repo pieces are sanitized and the trusted\n * session id is kept as a single path segment under the configured local root.\n */\n computeLocalSessionWorkdir(repoFullName: string, sessionId: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n if (this.#config.machine.provider !== 'local') {\n throw new Error('Local session workdirs require the local sandbox provider');\n }\n\n const localRoot = (this.#config.machine as { workingDirectory?: unknown }).workingDirectory;\n if (typeof localRoot !== 'string' || localRoot.length === 0) {\n throw new Error('Local sandbox working directory is not configured');\n }\n\n const [owner, name] = repoFullName.split('/', 2);\n return resolveContainedLocalWorkdir(\n localRoot,\n 'github-sessions',\n sanitizeSegment(owner || 'unknown'),\n sanitizeSegment(name || 'repo'),\n sanitizeSegment(sessionId),\n );\n }\n\n /**\n * Build a (not-yet-started) sandbox: the test-provided factory when set,\n * otherwise a per-project clone of the configured machine. The stored id is\n * passed both as the logical `id` (providers that reattach by construction\n * id, e.g. local) and as the provider-native `sandboxId` hint (Railway) so\n * reattach works across the provider matrix.\n *\n * `env` is deliberately NOT forwarded to the provider clone: remote\n * providers bake creation-time env into the VM for its whole lifetime\n * (`POST /sandbox`), which would persist credentials like `GH_TOKEN` inside\n * a VM that can outlive the session and be reused by another user via the\n * sandbox pool. Instead the env lives only on the adapter, which merges it\n * into every `executeCommand` — commands see the (refreshable) token, but\n * the VM itself never stores it.\n */\n #build(opts: SandboxCreateOptions): MaterializationSandbox {\n if (this.#factory) return this.#factory(opts);\n if (!this.#config) throw new Error('No sandbox configured');\n const clone = this.#config.machine.clone!({\n ...(opts.providerSandboxId ? { id: opts.providerSandboxId, sandboxId: opts.providerSandboxId } : {}),\n ...(opts.workingDirectory ? { workingDirectory: opts.workingDirectory } : {}),\n ...(opts.idleTimeoutMinutes !== undefined ? { idleTimeoutMinutes: opts.idleTimeoutMinutes } : {}),\n ...(opts.checkpointName ? { checkpointName: opts.checkpointName } : {}),\n });\n return toMaterializationSandbox(clone, opts.env);\n }\n\n /**\n * Provision a new sandbox (persisting its provider id on first open) or\n * reattach to the stored one. Returns a started, live sandbox.\n *\n * Concurrent calls for the same binding coalesce onto one in-flight\n * provision/reattach and share its sandbox handle — N simultaneous requests\n * for one cold session (e.g. several browser tabs polling right after boot)\n * must not each fire their own `POST /sandbox` against the provider.\n * Failures are not cached: once the shared attempt settles, the next call\n * starts fresh.\n */\n async ensureSandbox(store: SandboxBindingStore, onProgress?: ProgressFn): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n env?: Record<string, string>,\n onProgress?: ProgressFn,\n options?: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n envOrProgress?: Record<string, string> | ProgressFn,\n progressOrOptions?: ProgressFn | EnsureSandboxOptions,\n maybeOptions: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const env = typeof envOrProgress === 'function' ? undefined : envOrProgress;\n const onProgress =\n typeof envOrProgress === 'function' ? envOrProgress : (progressOrOptions as ProgressFn | undefined);\n const options =\n typeof envOrProgress === 'function'\n ? ((progressOrOptions as EnsureSandboxOptions | undefined) ?? {})\n : maybeOptions;\n\n const key = coalesceKey(store);\n if (!key) return this.#ensureSandboxUncoalesced(store, env, onProgress, options);\n\n const existing = this.#inflight.get(key);\n if (existing) return existing;\n\n const promise = this.#ensureSandboxUncoalesced(store, env, onProgress, options).finally(() => {\n // Only clear when this is still the entry we own.\n if (this.#inflight.get(key) === promise) this.#inflight.delete(key);\n });\n this.#inflight.set(key, promise);\n return promise;\n }\n\n /** The single provision/reattach attempt behind {@link ensureSandbox}. */\n async #ensureSandboxUncoalesced(\n store: SandboxBindingStore,\n env: Record<string, string> | undefined,\n onProgress: ProgressFn | undefined,\n options: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox> {\n const idleTimeoutMinutes = this.idleMinutes;\n const checkpointName = store.checkpointName;\n\n // Reattach path: if we have a stored sandbox id, try to reattach. The VM may\n // have been torn down by the provider's idle GC (or otherwise died), in which\n // case `start()` fails. Recover by clearing the stale id and provisioning a\n // fresh sandbox so the next open succeeds instead of being permanently wedged.\n if (store.sandboxId) {\n reportProgress(onProgress, { phase: 'reattaching', message: 'Reconnecting to your sandbox…' });\n const reattached = this.#build({\n providerSandboxId: store.sandboxId,\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n try {\n await reattached.start();\n return reattached;\n } catch {\n await store.setSandboxId(null);\n // fall through to fresh provision below\n }\n }\n\n // Fresh provision: enforce the per-replica budget before spending quota.\n const max = this.maxSandboxes;\n if (max > 0 && this.#liveCount >= max) {\n throw new SandboxBudgetError(max);\n }\n\n reportProgress(onProgress, { phase: 'provisioning', message: 'Provisioning a new sandbox…' });\n const sandbox = this.#build({\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n await sandbox.start();\n this.#liveCount += 1;\n\n const providerSandboxId = await readProviderSandboxId(sandbox);\n if (providerSandboxId) {\n await store.setSandboxId(providerSandboxId);\n }\n\n return sandbox;\n }\n\n /**\n * Tear down a sandbox binding: stop the live VM (best-effort) and clear the\n * persisted state through the binding store so the next open re-provisions\n * cleanly. Decrements the per-replica live-sandbox counter.\n *\n * @param store the binding to tear down\n * @param sandbox an already-reattached live sandbox to stop, when available\n */\n async teardownSandbox(store: SandboxBindingStore, sandbox?: MaterializationSandbox): Promise<void> {\n if (sandbox?.stop) {\n try {\n await sandbox.stop();\n } catch {\n // Best-effort: the VM may already be gone (idle GC). Still clear the binding.\n }\n }\n if (store.sandboxId) {\n if (this.#liveCount > 0) this.#liveCount -= 1;\n await store.clear();\n }\n }\n\n /**\n * Reattach to an already-provisioned sandbox by its provider id and start it.\n * Used by the workspace seam when opening a project that was already\n * materialized (sandbox id + workdir carried on controller state), so no DB\n * round-trip is needed.\n */\n async reattachSandbox(\n providerSandboxId: string,\n options: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const sandbox = this.#build({\n providerSandboxId,\n idleTimeoutMinutes: this.idleMinutes,\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n await sandbox.start();\n return sandbox;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"fleet.js","names":["#config","#inflight","#liveCount","#factory","#ensureSandboxUncoalesced","#build"],"sources":["../../src/sandbox/fleet.ts"],"sourcesContent":["/**\n * Project sandbox fleet: provisioning, reattach, teardown, and budgeting.\n *\n * Server-hosted projects never run on the web host itself. Each project gets\n * its own isolated sandbox (a `WorkspaceSandbox`, e.g. a Railway VM) `clone()`d\n * from the machine the factory was configured with. This module owns everything\n * about that fleet — which provider is active, where checkouts live inside a\n * sandbox, the idle window, the per-replica budget, and the\n * provision/reattach/teardown lifecycle — but knows nothing about what runs\n * inside a sandbox (git materialization lives with its feature, e.g. the\n * GitHub integration's `sandbox.ts`).\n *\n * The fleet is constructed once at boot with the machine config (or none, when\n * sandboxes are disabled) and handed to consumers — no global registry.\n * Persistence of the provider's reattach id is delegated to the caller via\n * {@link SandboxBindingStore}, so the fleet stays storage-agnostic. Tests can\n * swap the low-level construction via {@link SandboxFleet.setFactory}.\n */\n\nimport path from 'node:path';\n\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\nimport { timedPhase } from '../timing.js';\n\n/** Minimal command result shape sandbox consumers depend on. */\nexport interface SandboxCommandResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\n/**\n * Minimal live-sandbox surface fleet consumers need: an id, a way to start it,\n * a way to learn the provider's reattach id, and command execution.\n */\nexport interface MaterializationSandbox {\n readonly id: string;\n start(): Promise<void>;\n getInfo(): Promise<{ metadata?: Record<string, unknown> }>;\n executeCommand(\n command: string,\n args?: string[],\n options?: { timeout?: number; env?: Record<string, string | undefined> },\n ): Promise<SandboxCommandResult>;\n /** Update an environment variable for future commands in this sandbox. */\n setEnvironmentVariable?(name: string, value: string): void;\n /** Tear down the underlying VM. Optional: providers without it are no-ops. */\n stop?(): Promise<void>;\n}\n\n/** Options for building (or reattaching) one sandbox. */\nexport interface SandboxCreateOptions {\n /** Reattach to this existing provider VM instead of provisioning a new one. */\n providerSandboxId?: string;\n /**\n * Environment variables for commands run in the sandbox. Adapter-level\n * only: merged into every `executeCommand`, never baked into the provider\n * VM (see `SandboxFleet.#build`).\n */\n env?: Record<string, string>;\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n /** Idle teardown window (minutes). The provider stops the VM after this idle period. */\n idleTimeoutMinutes?: number;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n checkpointName?: string;\n}\n\n/**\n * A coarse-grained step of the sandbox-preparation flow, reported as it happens\n * so the UI can show the user what the server is doing instead of a static\n * \"Preparing…\" toast. `phase` is a stable machine token; `message` is\n * user-facing copy.\n */\nexport interface PrepareProgress {\n phase: 'reattaching' | 'provisioning' | 'preparing-workspace' | 'cloning' | 'pulling' | 'finalizing' | 'done';\n message: string;\n}\n\n/** Callback invoked with each preparation step. Best-effort; never throws. */\nexport type ProgressFn = (event: PrepareProgress) => void;\n\n/** Invoke a progress callback without letting it break the actual work. */\nexport function reportProgress(onProgress: ProgressFn | undefined, event: PrepareProgress): void {\n if (!onProgress) return;\n try {\n onProgress(event);\n } catch {\n // Progress reporting must never break the actual work.\n }\n}\n\n/**\n * Factory that builds a (not-yet-started) sandbox. When `providerSandboxId` is\n * provided the sandbox should reattach to that existing VM instead of\n * provisioning a new one.\n */\nexport type SandboxFactory = (opts: SandboxCreateOptions) => MaterializationSandbox;\n\n/** Raised when provisioning would exceed the per-replica sandbox budget. */\nexport class SandboxBudgetError extends Error {\n readonly code = 'sandbox-budget-exceeded' as const;\n constructor(readonly max: number) {\n super(\n `Sandbox budget exceeded: this server already has ${max} active sandbox(es), ` +\n `the configured per-replica maximum. Close an existing repository's sandbox and try again.`,\n );\n this.name = 'SandboxBudgetError';\n }\n}\n\n/** Optional knobs for provisioning/reattaching one sandbox. */\nexport interface EnsureSandboxOptions {\n /** Provider working directory for this sandbox. */\n workingDirectory?: string;\n}\n\n/**\n * Where a feature persists its sandbox binding. The fleet reads the stored\n * reattach id and writes updates through this seam so it stays agnostic of\n * the owning table (GitHub projects today, anything else tomorrow).\n */\nexport interface SandboxBindingStore {\n /** Stored provider reattach id from a previous provisioning, if any. */\n readonly sandboxId: string | null;\n /** Provider checkpoint used to seed and preserve this sandbox's filesystem. */\n readonly checkpointName?: string;\n /** Persist a freshly provisioned provider id, or clear a stale one with `null`. */\n setSandboxId(id: string | null): Promise<void>;\n /** Clear all stored sandbox state (reattach id + materialization mark) on teardown. */\n clear(): Promise<void>;\n}\n\n/**\n * Stable identity for one binding's in-flight provision work, used to coalesce\n * concurrent `ensureSandbox` calls. Prefer `checkpointName` — it is a pure\n * function of the owning session and is set before the first provision, which\n * is exactly when the herd forms (the stored `sandboxId` is still null then).\n * Fall back to the stored provider id, and skip coalescing entirely for\n * bindings with neither: keying those on a shared constant would wrongly\n * funnel *different* bindings onto one sandbox.\n */\nfunction coalesceKey(store: SandboxBindingStore): string | undefined {\n if (store.checkpointName) return `checkpoint:${store.checkpointName}`;\n if (store.sandboxId) return `sandbox:${store.sandboxId}`;\n return undefined;\n}\n\n/**\n * Adapt a cloned `WorkspaceSandbox` to the minimal surface this module needs.\n * Lifecycle goes through the `_`-prefixed wrappers when present (they add\n * status tracking and concurrency safety on `MastraSandbox` subclasses),\n * falling back to the plain methods for interface-only implementations.\n */\nfunction toMaterializationSandbox(\n sandbox: WorkspaceSandbox,\n initialEnvironment: Record<string, string> = {},\n): MaterializationSandbox {\n if (typeof sandbox.executeCommand !== 'function') {\n throw new Error(\n `Sandbox provider '${sandbox.provider}' does not implement executeCommand() — cannot materialize repos.`,\n );\n }\n const lifecycle = sandbox as { _start?(): Promise<void>; _stop?(): Promise<void> };\n const environment = { ...initialEnvironment };\n return {\n id: sandbox.id,\n start: async () => {\n await (lifecycle._start ?? sandbox.start)?.call(sandbox);\n },\n getInfo: async () => (await sandbox.getInfo?.()) ?? {},\n executeCommand: (command, args, options) =>\n sandbox.executeCommand!(command, args, {\n ...options,\n env: { ...environment, ...options?.env },\n }),\n setEnvironmentVariable: (name, value) => {\n environment[name] = value;\n },\n stop: async () => {\n await (lifecycle._stop ?? sandbox.stop)?.call(sandbox);\n },\n };\n}\n\n/**\n * The provider's reattach id for a started sandbox. For Railway this is the\n * underlying `railwaySandboxId` in `getInfo().metadata`. Providers without a\n * provider-native id (e.g. local) reattach by construction id, so fall back\n * to the sandbox's own logical id.\n */\nasync function readProviderSandboxId(sandbox: MaterializationSandbox): Promise<string | undefined> {\n const info = await sandbox.getInfo();\n const id = info.metadata?.railwaySandboxId ?? info.metadata?.sandboxId;\n return typeof id === 'string' ? id : sandbox.id;\n}\n\n/** Keep each path piece a single safe segment (no separators or traversal). */\nfunction sanitizeSegment(segment: string): string {\n const cleaned = segment.replace(/[^A-Za-z0-9._-]/g, '-').replace(/^\\.+/, '');\n return cleaned || 'repo';\n}\n\n/** Resolve a workdir under `root`, refusing any path that escapes the configured root. */\nexport function resolveContainedLocalWorkdir(root: string, ...segments: string[]): string {\n const resolvedRoot = path.resolve(root);\n const resolved = path.resolve(resolvedRoot, ...segments);\n if (resolved !== resolvedRoot && resolved.startsWith(`${resolvedRoot}${path.sep}`)) return resolved;\n throw new Error(`Refusing to use local sandbox path outside configured root: ${resolved}`);\n}\n\n/**\n * Factory-resolved sandbox runtime the fleet is constructed with: the machine\n * projects clone their per-project sandboxes from, plus the knobs the factory\n * resolved around it.\n */\nexport interface SandboxFleetConfig {\n /**\n * Template machine (validated by the factory to implement `clone()`).\n * Never started — acts purely as the credential/default holder that\n * per-project sandboxes are cloned from.\n */\n machine: WorkspaceSandbox;\n /** In-sandbox base directory repos check out under (no trailing slash). */\n workdirBase: string;\n /** Per-replica cap on concurrently provisioned sandboxes. 0 = unlimited. */\n maxSandboxes?: number;\n}\n\n/**\n * The sandbox fleet for one deployment. Constructed once at boot — with a\n * config when a sandbox machine was configured, or without one when sandboxes\n * are disabled (every provisioning entry point then throws and\n * {@link enabled} reports `false` so features stay off).\n */\nexport class SandboxFleet {\n readonly #config: SandboxFleetConfig | undefined;\n #factory: SandboxFactory | undefined;\n #liveCount = 0;\n /** In-flight `ensureSandbox` work, keyed per binding so concurrent callers coalesce. */\n readonly #inflight = new Map<string, Promise<MaterializationSandbox>>();\n\n constructor(config?: SandboxFleetConfig) {\n this.#config = config;\n }\n\n /**\n * True when a sandbox machine was configured. The factory validates the\n * machine implements `clone()` at boot, so a configured fleet is usable —\n * sandbox-backed projects stay off only when the slot was omitted.\n */\n get enabled(): boolean {\n return this.#config !== undefined;\n }\n\n /**\n * Name of the active sandbox provider — the configured machine's `provider`\n * discriminator (`'railway'`, `'local'`, …), or `'none'` when the fleet was\n * constructed without a config. Diagnostic only; feature gating goes\n * through {@link enabled}.\n */\n get provider(): string {\n return this.#config?.machine.provider ?? 'none';\n }\n\n /**\n * Idle teardown window for provisioned sandboxes, in minutes; defaults to 30.\n * Read back from the machine's own config when it exposes one\n * (Railway's `idleTimeoutMinutes`) — the knob lives on the sandbox, the\n * fleet only needs it to schedule GC and stamp sandbox clones. Advisory:\n * providers without idle GC ignore it, and a re-open detects a torn-down VM\n * and re-provisions cleanly.\n */\n get idleMinutes(): number {\n const machine = this.#config?.machine as { idleTimeoutMinutes?: unknown } | undefined;\n const minutes = machine?.idleTimeoutMinutes;\n return typeof minutes === 'number' && Number.isFinite(minutes) && minutes > 0 ? minutes : 30;\n }\n\n /**\n * Per-replica cap on concurrently *provisioned* sandboxes. 0 means unlimited.\n * This is a lightweight per-process budget to keep a single replica from\n * exhausting provider quota — it is not a global, cross-replica scheduler\n * (that is a deferred follow-up).\n */\n get maxSandboxes(): number {\n return this.#config?.maxSandboxes ?? 0;\n }\n\n /**\n * Count of sandboxes this fleet has freshly provisioned and not yet torn\n * down. Reattaches to existing VMs do not count (they reuse an already-billed\n * sandbox). Used to enforce {@link maxSandboxes}.\n */\n get liveCount(): number {\n return this.#liveCount;\n }\n\n /** For tests: reset the live-sandbox counter to a known state. */\n __resetLiveCount(value = 0): void {\n this.#liveCount = value;\n }\n\n /** Override the sandbox factory (tests). */\n setFactory(factory: SandboxFactory): void {\n this.#factory = factory;\n }\n\n /** Reset to the default machine-cloning factory. */\n resetFactory(): void {\n this.#factory = undefined;\n }\n\n /**\n * Compute the in-sandbox working directory for a repo: a nested\n * `<base>/<owner>/<name>` layout under the factory-resolved checkout base.\n * Nesting keeps same-name repos apart (`acme/api` vs `other/api`) — cloud\n * sandboxes are one-per-project so it's merely tidy there, but local\n * checkouts share one host root where it prevents collisions. Server-side\n * only; never derived from client input.\n */\n computeWorkdir(repoFullName: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n const [owner, name] = repoFullName.split('/', 2);\n return `${this.#config.workdirBase}/${sanitizeSegment(owner || 'unknown')}/${sanitizeSegment(name || 'repo')}`;\n }\n\n /**\n * Compute the host working directory for a local GitHub session checkout.\n * This is server-derived only: repo pieces are sanitized and the trusted\n * session id is kept as a single path segment under the configured local root.\n */\n computeLocalSessionWorkdir(repoFullName: string, sessionId: string): string {\n if (!this.#config) throw new Error('No sandbox configured');\n if (this.#config.machine.provider !== 'local') {\n throw new Error('Local session workdirs require the local sandbox provider');\n }\n\n const localRoot = (this.#config.machine as { workingDirectory?: unknown }).workingDirectory;\n if (typeof localRoot !== 'string' || localRoot.length === 0) {\n throw new Error('Local sandbox working directory is not configured');\n }\n\n const [owner, name] = repoFullName.split('/', 2);\n return resolveContainedLocalWorkdir(\n localRoot,\n 'github-sessions',\n sanitizeSegment(owner || 'unknown'),\n sanitizeSegment(name || 'repo'),\n sanitizeSegment(sessionId),\n );\n }\n\n /**\n * Build a (not-yet-started) sandbox: the test-provided factory when set,\n * otherwise a per-project clone of the configured machine. The stored id is\n * passed both as the logical `id` (providers that reattach by construction\n * id, e.g. local) and as the provider-native `sandboxId` hint (Railway) so\n * reattach works across the provider matrix.\n *\n * `env` is deliberately NOT forwarded to the provider clone: remote\n * providers bake creation-time env into the VM for its whole lifetime\n * (`POST /sandbox`), which would persist credentials like `GH_TOKEN` inside\n * a VM that can outlive the session and be reused by another user via the\n * sandbox pool. Instead the env lives only on the adapter, which merges it\n * into every `executeCommand` — commands see the (refreshable) token, but\n * the VM itself never stores it.\n */\n #build(opts: SandboxCreateOptions): MaterializationSandbox {\n if (this.#factory) return this.#factory(opts);\n if (!this.#config) throw new Error('No sandbox configured');\n const clone = this.#config.machine.clone!({\n ...(opts.providerSandboxId ? { id: opts.providerSandboxId, sandboxId: opts.providerSandboxId } : {}),\n ...(opts.workingDirectory ? { workingDirectory: opts.workingDirectory } : {}),\n ...(opts.idleTimeoutMinutes !== undefined ? { idleTimeoutMinutes: opts.idleTimeoutMinutes } : {}),\n ...(opts.checkpointName ? { checkpointName: opts.checkpointName } : {}),\n });\n return toMaterializationSandbox(clone, opts.env);\n }\n\n /**\n * Provision a new sandbox (persisting its provider id on first open) or\n * reattach to the stored one. Returns a started, live sandbox.\n *\n * Concurrent calls for the same binding coalesce onto one in-flight\n * provision/reattach and share its sandbox handle — N simultaneous requests\n * for one cold session (e.g. several browser tabs polling right after boot)\n * must not each fire their own `POST /sandbox` against the provider.\n * Failures are not cached: once the shared attempt settles, the next call\n * starts fresh.\n */\n async ensureSandbox(store: SandboxBindingStore, onProgress?: ProgressFn): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n env?: Record<string, string>,\n onProgress?: ProgressFn,\n options?: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox>;\n async ensureSandbox(\n store: SandboxBindingStore,\n envOrProgress?: Record<string, string> | ProgressFn,\n progressOrOptions?: ProgressFn | EnsureSandboxOptions,\n maybeOptions: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const env = typeof envOrProgress === 'function' ? undefined : envOrProgress;\n const onProgress =\n typeof envOrProgress === 'function' ? envOrProgress : (progressOrOptions as ProgressFn | undefined);\n const options =\n typeof envOrProgress === 'function'\n ? ((progressOrOptions as EnsureSandboxOptions | undefined) ?? {})\n : maybeOptions;\n\n const key = coalesceKey(store);\n if (!key) return this.#ensureSandboxUncoalesced(store, env, onProgress, options);\n\n const existing = this.#inflight.get(key);\n if (existing) return existing;\n\n const promise = this.#ensureSandboxUncoalesced(store, env, onProgress, options).finally(() => {\n // Only clear when this is still the entry we own.\n if (this.#inflight.get(key) === promise) this.#inflight.delete(key);\n });\n this.#inflight.set(key, promise);\n return promise;\n }\n\n /** The single provision/reattach attempt behind {@link ensureSandbox}. */\n async #ensureSandboxUncoalesced(\n store: SandboxBindingStore,\n env: Record<string, string> | undefined,\n onProgress: ProgressFn | undefined,\n options: EnsureSandboxOptions,\n ): Promise<MaterializationSandbox> {\n const idleTimeoutMinutes = this.idleMinutes;\n const checkpointName = store.checkpointName;\n\n // Reattach path: if we have a stored sandbox id, try to reattach. The VM may\n // have been torn down by the provider's idle GC (or otherwise died), in which\n // case `start()` fails. Recover by clearing the stale id and provisioning a\n // fresh sandbox so the next open succeeds instead of being permanently wedged.\n if (store.sandboxId) {\n reportProgress(onProgress, { phase: 'reattaching', message: 'Reconnecting to your sandbox…' });\n const reattached = this.#build({\n providerSandboxId: store.sandboxId,\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n try {\n await timedPhase('sandbox.reattach', () => reattached.start());\n return reattached;\n } catch {\n await store.setSandboxId(null);\n // fall through to fresh provision below\n }\n }\n\n // Fresh provision: enforce the per-replica budget before spending quota.\n const max = this.maxSandboxes;\n if (max > 0 && this.#liveCount >= max) {\n throw new SandboxBudgetError(max);\n }\n\n reportProgress(onProgress, { phase: 'provisioning', message: 'Provisioning a new sandbox…' });\n const sandbox = this.#build({\n idleTimeoutMinutes,\n ...(checkpointName ? { checkpointName } : {}),\n ...(env ? { env } : {}),\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n await timedPhase('sandbox.provision', () => sandbox.start());\n this.#liveCount += 1;\n\n const providerSandboxId = await readProviderSandboxId(sandbox);\n if (providerSandboxId) {\n await store.setSandboxId(providerSandboxId);\n }\n\n return sandbox;\n }\n\n /**\n * Tear down a sandbox binding: stop the live VM (best-effort) and clear the\n * persisted state through the binding store so the next open re-provisions\n * cleanly. Decrements the per-replica live-sandbox counter.\n *\n * @param store the binding to tear down\n * @param sandbox an already-reattached live sandbox to stop, when available\n */\n async teardownSandbox(store: SandboxBindingStore, sandbox?: MaterializationSandbox): Promise<void> {\n if (sandbox?.stop) {\n try {\n await sandbox.stop();\n } catch {\n // Best-effort: the VM may already be gone (idle GC). Still clear the binding.\n }\n }\n if (store.sandboxId) {\n if (this.#liveCount > 0) this.#liveCount -= 1;\n await store.clear();\n }\n }\n\n /**\n * Reattach to an already-provisioned sandbox by its provider id and start it.\n * Used by the workspace seam when opening a project that was already\n * materialized (sandbox id + workdir carried on controller state), so no DB\n * round-trip is needed.\n */\n async reattachSandbox(\n providerSandboxId: string,\n options: EnsureSandboxOptions = {},\n ): Promise<MaterializationSandbox> {\n const sandbox = this.#build({\n providerSandboxId,\n idleTimeoutMinutes: this.idleMinutes,\n ...(options.workingDirectory ? { workingDirectory: options.workingDirectory } : {}),\n });\n await sandbox.start();\n return sandbox;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { SessionBeforeAgentEndListener } from '@mastra/core/agent-controller';
2
+ import type { WorkspaceSandbox } from '@mastra/core/workspace';
3
+ export interface CheckpointCaptureSession {
4
+ getWorkspace(): {
5
+ sandbox?: Pick<WorkspaceSandbox, 'snapshot'>;
6
+ } | undefined;
7
+ onBeforeAgentEnd(listener: SessionBeforeAgentEndListener): () => void;
8
+ }
9
+ /**
10
+ * Snapshot the session's sandbox before every agent-end event so providers
11
+ * with checkpoint support (for example Railway-backed sandboxes) persist the
12
+ * last completed turn's writes. Captures are chained sequentially so a slow
13
+ * snapshot never overlaps the next one, and failures are logged rather than
14
+ * thrown so they never break the agent turn.
15
+ */
16
+ export declare function observeSessionCheckpoint(session: CheckpointCaptureSession): () => void;
17
+ //# sourceMappingURL=checkpoint-capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint-capture.d.ts","sourceRoot":"","sources":["../../src/session/checkpoint-capture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,wBAAwB;IACvC,YAAY,IAAI;QAAE,OAAO,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,CAAA;KAAE,GAAG,SAAS,CAAC;IAC7E,gBAAgB,CAAC,QAAQ,EAAE,6BAA6B,GAAG,MAAM,IAAI,CAAC;CACvE;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,wBAAwB,GAAG,MAAM,IAAI,CAetF"}
@@ -0,0 +1,27 @@
1
+ //#region src/session/checkpoint-capture.ts
2
+ /**
3
+ * Snapshot the session's sandbox before every agent-end event so providers
4
+ * with checkpoint support (for example Railway-backed sandboxes) persist the
5
+ * last completed turn's writes. Captures are chained sequentially so a slow
6
+ * snapshot never overlaps the next one, and failures are logged rather than
7
+ * thrown so they never break the agent turn.
8
+ */
9
+ function observeSessionCheckpoint(session) {
10
+ let capture = Promise.resolve();
11
+ return session.onBeforeAgentEnd(() => {
12
+ capture = capture.then(async () => {
13
+ const sandbox = session.getWorkspace()?.sandbox;
14
+ if (!sandbox) return;
15
+ try {
16
+ await sandbox.snapshot();
17
+ } catch (error) {
18
+ console.warn("[Factory checkpoint capture] Unable to snapshot sandbox.", error);
19
+ }
20
+ });
21
+ return capture;
22
+ });
23
+ }
24
+ //#endregion
25
+ export { observeSessionCheckpoint };
26
+
27
+ //# sourceMappingURL=checkpoint-capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint-capture.js","names":[],"sources":["../../src/session/checkpoint-capture.ts"],"sourcesContent":["import type { SessionBeforeAgentEndListener } from '@mastra/core/agent-controller';\nimport type { WorkspaceSandbox } from '@mastra/core/workspace';\n\nexport interface CheckpointCaptureSession {\n getWorkspace(): { sandbox?: Pick<WorkspaceSandbox, 'snapshot'> } | undefined;\n onBeforeAgentEnd(listener: SessionBeforeAgentEndListener): () => void;\n}\n\n/**\n * Snapshot the session's sandbox before every agent-end event so providers\n * with checkpoint support (for example Railway-backed sandboxes) persist the\n * last completed turn's writes. Captures are chained sequentially so a slow\n * snapshot never overlaps the next one, and failures are logged rather than\n * thrown so they never break the agent turn.\n */\nexport function observeSessionCheckpoint(session: CheckpointCaptureSession): () => void {\n let capture = Promise.resolve();\n return session.onBeforeAgentEnd(() => {\n capture = capture.then(async () => {\n // Chat-only sessions run without a workspace; there is nothing to snapshot.\n const sandbox = session.getWorkspace()?.sandbox;\n if (!sandbox) return;\n try {\n await sandbox.snapshot();\n } catch (error) {\n console.warn('[Factory checkpoint capture] Unable to snapshot sandbox.', error);\n }\n });\n return capture;\n });\n}\n"],"mappings":";;;;;;;;AAeA,SAAgB,yBAAyB,SAA+C;CACtF,IAAI,UAAU,QAAQ,QAAQ;CAC9B,OAAO,QAAQ,uBAAuB;EACpC,UAAU,QAAQ,KAAK,YAAY;GAEjC,MAAM,UAAU,QAAQ,aAAa,CAAC,EAAE;GACxC,IAAI,CAAC,SAAS;GACd,IAAI;IACF,MAAM,QAAQ,SAAS;GACzB,SAAS,OAAO;IACd,QAAQ,KAAK,4DAA4D,KAAK;GAChF;EACF,CAAC;EACD,OAAO;CACT,CAAC;AACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"factory-session.d.ts","sourceRoot":"","sources":["../../src/session/factory-session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAErE,OAAO,KAAK,EAAwB,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AAC9G,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;AAE7F;;;GAGG;AACH,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,sBAAsB,GAAG,SAAS,EAC5C,gBAAgB,EAAE,MAAM,GAAG,SAAS,GACnC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B;AAED,MAAM,WAAW,8BAA8B;IAC7C;;;;;OAKG;IACH,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,MAAM,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,+BAA+B;IAC9C,mBAAmB,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,MAAM,6BAA6B,GACrC,CAAC;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG,+BAA+B,CAAC,GACnD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAAA;CAAE,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CAAC,IAAI,EAAE;IACzD,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,OAAO,CAAC,6BAA6B,CAAC,CAyBzC;AAED;;;;;;;;GAQG;AACH,wBAAsB,+BAA+B,CAAC,IAAI,EAAE;IAC1D,aAAa,EAAE,0BAA0B,CAAC;IAC1C,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAc9E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,8BAA8B,GACnC,OAAO,CAAC,2BAA2B,CAAC,CA4BtC;AAcD,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yGAAyG;IACzG,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACxC;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBnH"}
1
+ {"version":3,"file":"factory-session.d.ts","sourceRoot":"","sources":["../../src/session/factory-session.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAErE,OAAO,KAAK,EAAwB,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AAC9G,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAE5F,KAAK,cAAc,GAAG,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,eAAe,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;AAE7F;;;GAGG;AACH,wBAAsB,4BAA4B,CAChD,QAAQ,EAAE,sBAAsB,GAAG,SAAS,EAC5C,gBAAgB,EAAE,MAAM,GAAG,SAAS,GACnC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B;AAED,MAAM,WAAW,8BAA8B;IAC7C;;;;;OAKG;IACH,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,2BAA2B;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,MAAM,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,+BAA+B;IAC9C,mBAAmB,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,UAAU,EAAE,MAAM,CAAC;IACnB,uFAAuF;IACvF,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,MAAM,6BAA6B,GACrC,CAAC;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GAAG,+BAA+B,CAAC,GACnD;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAAA;CAAE,CAAC;AAE1D;;;;;;GAMG;AACH,wBAAsB,8BAA8B,CAAC,IAAI,EAAE;IACzD,aAAa,EAAE,0BAA0B,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,gBAAgB,EAAE,MAAM,CAAC;IACzB,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,GAAG,OAAO,CAAC,6BAA6B,CAAC,CAuCzC;AAED;;;;;;;;GAQG;AACH,wBAAsB,+BAA+B,CAAC,IAAI,EAAE;IAC1D,aAAa,EAAE,0BAA0B,CAAC;IAC1C,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAc9E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,8BAA8B,GACnC,OAAO,CAAC,2BAA2B,CAAC,CA4BtC;AAcD,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,yGAAyG;IACzG,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yGAAyG;IACzG,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACxC;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBnH"}
@@ -21,35 +21,43 @@ async function resolveFactoryDefaultModelId(projects, factoryProjectId) {
21
21
  */
22
22
  async function resolveFactorySourceRepository(args) {
23
23
  const { sourceControl, orgId, factoryProjectId, repositorySlug } = args;
24
- const connection = (await sourceControl.connections.list({
24
+ const candidates = (await sourceControl.connections.list({
25
25
  orgId,
26
26
  factoryProjectId
27
- })).find((candidate) => candidate.integrationId === sourceControl.integrationId);
28
- if (!connection) return {
27
+ })).filter((candidate) => candidate.integrationId === sourceControl.integrationId);
28
+ if (candidates.length === 0) return {
29
29
  found: false,
30
30
  reason: "connection"
31
31
  };
32
- const projectRepositories = await sourceControl.projectRepositories.list({
33
- orgId,
34
- connectionId: connection.id
35
- });
36
- const resolved = (await Promise.all(projectRepositories.map(async (projectRepository) => ({
37
- projectRepository,
38
- repository: await sourceControl.repositories.get({
39
- orgId,
40
- id: projectRepository.repositoryId
41
- })
42
- })))).find((candidate) => candidate.repository && (!repositorySlug || candidate.repository.slug === repositorySlug));
43
- if (!resolved?.repository) return {
32
+ for (const connection of candidates) {
33
+ let resolved;
34
+ try {
35
+ const projectRepositories = await sourceControl.projectRepositories.list({
36
+ orgId,
37
+ connectionId: connection.id
38
+ });
39
+ resolved = (await Promise.all(projectRepositories.map(async (projectRepository) => ({
40
+ projectRepository,
41
+ repository: await sourceControl.repositories.get({
42
+ orgId,
43
+ id: projectRepository.repositoryId
44
+ })
45
+ })))).find((candidate) => candidate.repository && (!repositorySlug || candidate.repository.slug === repositorySlug));
46
+ } catch {
47
+ continue;
48
+ }
49
+ if (!resolved?.repository) continue;
50
+ return {
51
+ found: true,
52
+ projectRepositoryId: resolved.projectRepository.id,
53
+ baseBranch: resolved.projectRepository.branch ?? resolved.repository.defaultBranch,
54
+ connectedByUserId: connection.createdByUserId
55
+ };
56
+ }
57
+ return {
44
58
  found: false,
45
59
  reason: "repository"
46
60
  };
47
- return {
48
- found: true,
49
- projectRepositoryId: resolved.projectRepository.id,
50
- baseBranch: resolved.projectRepository.branch ?? resolved.repository.defaultBranch,
51
- connectedByUserId: connection.createdByUserId
52
- };
53
61
  }
54
62
  /**
55
63
  * Walk a Factory user-session id back to the project it belongs to.