@mastra/factory 0.5.1-alpha.0 → 0.6.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/dist/auth.d.ts +2 -0
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +2 -0
  5. package/dist/auth.js.map +1 -1
  6. package/dist/capabilities/intake.d.ts +1 -0
  7. package/dist/capabilities/intake.d.ts.map +1 -1
  8. package/dist/capabilities/version-control.d.ts +3 -0
  9. package/dist/capabilities/version-control.d.ts.map +1 -1
  10. package/dist/factory.d.ts +10 -0
  11. package/dist/factory.d.ts.map +1 -1
  12. package/dist/factory.js +15 -2
  13. package/dist/factory.js.map +1 -1
  14. package/dist/integrations/base.d.ts +6 -0
  15. package/dist/integrations/base.d.ts.map +1 -1
  16. package/dist/integrations/github/integration.d.ts +14 -1
  17. package/dist/integrations/github/integration.d.ts.map +1 -1
  18. package/dist/integrations/github/integration.js +61 -3
  19. package/dist/integrations/github/integration.js.map +1 -1
  20. package/dist/integrations/github/issue-reconciler.d.ts +24 -0
  21. package/dist/integrations/github/issue-reconciler.d.ts.map +1 -0
  22. package/dist/integrations/github/issue-reconciler.js +104 -0
  23. package/dist/integrations/github/issue-reconciler.js.map +1 -0
  24. package/dist/integrations/github/reconcile-worker.d.ts +8 -0
  25. package/dist/integrations/github/reconcile-worker.d.ts.map +1 -1
  26. package/dist/integrations/github/reconcile-worker.js +38 -1
  27. package/dist/integrations/github/reconcile-worker.js.map +1 -1
  28. package/dist/integrations/github/routes.d.ts.map +1 -1
  29. package/dist/integrations/github/routes.js +65 -52
  30. package/dist/integrations/github/routes.js.map +1 -1
  31. package/dist/integrations/github/rules.d.ts +37 -2
  32. package/dist/integrations/github/rules.d.ts.map +1 -1
  33. package/dist/integrations/github/rules.js +150 -14
  34. package/dist/integrations/github/rules.js.map +1 -1
  35. package/dist/integrations/github/sandbox-release.d.ts +16 -1
  36. package/dist/integrations/github/sandbox-release.d.ts.map +1 -1
  37. package/dist/integrations/github/sandbox-release.js +42 -1
  38. package/dist/integrations/github/sandbox-release.js.map +1 -1
  39. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  40. package/dist/integrations/github/sandbox.js +74 -8
  41. package/dist/integrations/github/sandbox.js.map +1 -1
  42. package/dist/integrations/issue-reconcile-worker.d.ts +19 -0
  43. package/dist/integrations/issue-reconcile-worker.d.ts.map +1 -0
  44. package/dist/integrations/issue-reconcile-worker.js +98 -0
  45. package/dist/integrations/issue-reconcile-worker.js.map +1 -0
  46. package/dist/integrations/issue-reconciler.d.ts +47 -0
  47. package/dist/integrations/issue-reconciler.d.ts.map +1 -0
  48. package/dist/integrations/issue-reconciler.js +104 -0
  49. package/dist/integrations/issue-reconciler.js.map +1 -0
  50. package/dist/integrations/linear/integration.d.ts +4 -0
  51. package/dist/integrations/linear/integration.d.ts.map +1 -1
  52. package/dist/integrations/linear/integration.js +19 -2
  53. package/dist/integrations/linear/integration.js.map +1 -1
  54. package/dist/integrations/linear/issue-reconciler.d.ts +6 -0
  55. package/dist/integrations/linear/issue-reconciler.d.ts.map +1 -0
  56. package/dist/integrations/linear/issue-reconciler.js +64 -0
  57. package/dist/integrations/linear/issue-reconciler.js.map +1 -0
  58. package/dist/integrations/linear/routes.d.ts.map +1 -1
  59. package/dist/integrations/linear/routes.js +1 -0
  60. package/dist/integrations/linear/routes.js.map +1 -1
  61. package/dist/integrations/linear/rules.d.ts +2 -0
  62. package/dist/integrations/linear/rules.d.ts.map +1 -1
  63. package/dist/integrations/linear/rules.js +5 -2
  64. package/dist/integrations/linear/rules.js.map +1 -1
  65. package/dist/integrations/platform/github/event-worker.d.ts +7 -0
  66. package/dist/integrations/platform/github/event-worker.d.ts.map +1 -1
  67. package/dist/integrations/platform/github/event-worker.js +24 -0
  68. package/dist/integrations/platform/github/event-worker.js.map +1 -1
  69. package/dist/integrations/platform/github/integration.d.ts +13 -3
  70. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  71. package/dist/integrations/platform/github/integration.js +42 -1
  72. package/dist/integrations/platform/github/integration.js.map +1 -1
  73. package/dist/integrations/platform/linear/event-worker.d.ts +105 -0
  74. package/dist/integrations/platform/linear/event-worker.d.ts.map +1 -0
  75. package/dist/integrations/platform/linear/event-worker.js +380 -0
  76. package/dist/integrations/platform/linear/event-worker.js.map +1 -0
  77. package/dist/integrations/platform/linear/integration.d.ts +9 -0
  78. package/dist/integrations/platform/linear/integration.d.ts.map +1 -1
  79. package/dist/integrations/platform/linear/integration.js +37 -1
  80. package/dist/integrations/platform/linear/integration.js.map +1 -1
  81. package/dist/integrations/slack/integration.d.ts.map +1 -1
  82. package/dist/integrations/slack/integration.js +3 -2
  83. package/dist/integrations/slack/integration.js.map +1 -1
  84. package/dist/integrations/slack/slack.d.ts +37 -110
  85. package/dist/integrations/slack/slack.d.ts.map +1 -1
  86. package/dist/integrations/slack/slack.js +49 -44
  87. package/dist/integrations/slack/slack.js.map +1 -1
  88. package/dist/routes/config.d.ts +25 -0
  89. package/dist/routes/config.d.ts.map +1 -1
  90. package/dist/routes/config.js +63 -1
  91. package/dist/routes/config.js.map +1 -1
  92. package/dist/routes/fs.d.ts +16 -2
  93. package/dist/routes/fs.d.ts.map +1 -1
  94. package/dist/routes/fs.js +47 -5
  95. package/dist/routes/fs.js.map +1 -1
  96. package/dist/routes/surface.d.ts +13 -1
  97. package/dist/routes/surface.d.ts.map +1 -1
  98. package/dist/routes/surface.js +20 -9
  99. package/dist/routes/surface.js.map +1 -1
  100. package/dist/rules/binding-context.d.ts +28 -1
  101. package/dist/rules/binding-context.d.ts.map +1 -1
  102. package/dist/rules/binding-context.js +66 -1
  103. package/dist/rules/binding-context.js.map +1 -1
  104. package/dist/rules/defaults.d.ts.map +1 -1
  105. package/dist/rules/defaults.js +71 -5
  106. package/dist/rules/defaults.js.map +1 -1
  107. package/dist/rules/start-coordinator.d.ts.map +1 -1
  108. package/dist/rules/start-coordinator.js +7 -26
  109. package/dist/rules/start-coordinator.js.map +1 -1
  110. package/dist/rules/tools.d.ts +2 -0
  111. package/dist/rules/tools.d.ts.map +1 -1
  112. package/dist/rules/tools.js +15 -6
  113. package/dist/rules/tools.js.map +1 -1
  114. package/dist/rules/types.d.ts +17 -2
  115. package/dist/rules/types.d.ts.map +1 -1
  116. package/dist/rules/types.js +2 -1
  117. package/dist/rules/types.js.map +1 -1
  118. package/dist/session/factory-session.d.ts +116 -0
  119. package/dist/session/factory-session.d.ts.map +1 -0
  120. package/dist/session/factory-session.js +162 -0
  121. package/dist/session/factory-session.js.map +1 -0
  122. package/dist/session/filesystem-capture.d.ts +26 -0
  123. package/dist/session/filesystem-capture.d.ts.map +1 -0
  124. package/dist/session/filesystem-capture.js +75 -0
  125. package/dist/session/filesystem-capture.js.map +1 -0
  126. package/dist/storage/domains/audit/domain.d.ts +9 -2
  127. package/dist/storage/domains/audit/domain.d.ts.map +1 -1
  128. package/dist/storage/domains/audit/domain.js +106 -4
  129. package/dist/storage/domains/audit/domain.js.map +1 -1
  130. package/dist/storage/domains/filesystem/base.d.ts +26 -0
  131. package/dist/storage/domains/filesystem/base.d.ts.map +1 -0
  132. package/dist/storage/domains/filesystem/base.js +74 -0
  133. package/dist/storage/domains/filesystem/base.js.map +1 -0
  134. package/dist/storage/domains/projects/base.d.ts +1 -0
  135. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  136. package/dist/storage/domains/projects/base.js +3 -0
  137. package/dist/storage/domains/projects/base.js.map +1 -1
  138. package/dist/storage/domains/source-control/base.d.ts +2 -0
  139. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  140. package/dist/storage/domains/source-control/base.js +6 -0
  141. package/dist/storage/domains/source-control/base.js.map +1 -1
  142. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  143. package/dist/storage/domains/source-control/inmemory.js +3 -0
  144. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  145. package/dist/storage/domains/work-items/base.d.ts +11 -0
  146. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  147. package/dist/storage/domains/work-items/base.js +28 -0
  148. package/dist/storage/domains/work-items/base.js.map +1 -1
  149. package/factory-skills/factory-triage/SKILL.md +45 -12
  150. package/package.json +5 -5
  151. package/dist/integrations/github/factory-session.d.ts +0 -12
  152. package/dist/integrations/github/factory-session.d.ts.map +0 -1
  153. package/dist/integrations/github/factory-session.js +0 -37
  154. package/dist/integrations/github/factory-session.js.map +0 -1
@@ -13,6 +13,7 @@ const LEASE_KEY = "github:pull-request-reconcile";
13
13
  var GithubReconcileWorker = class extends MastraWorker {
14
14
  name = "github-pull-request-reconcile";
15
15
  #reconcile;
16
+ #reconcileIssues;
16
17
  #sourceControl;
17
18
  #intervalMs;
18
19
  #leaseTtlMs;
@@ -24,6 +25,7 @@ var GithubReconcileWorker = class extends MastraWorker {
24
25
  constructor(config) {
25
26
  super();
26
27
  this.#reconcile = config.reconcile;
28
+ this.#reconcileIssues = config.reconcileIssues;
27
29
  this.#sourceControl = config.sourceControl;
28
30
  this.#intervalMs = config.intervalMs ?? 3e5;
29
31
  if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) throw new Error("GitHub pull request reconcile interval must be a positive number.");
@@ -64,6 +66,21 @@ var GithubReconcileWorker = class extends MastraWorker {
64
66
  }
65
67
  async #tick() {
66
68
  if (!(await this.#leaseProvider.acquireLease(LEASE_KEY, this.#leaseOwner, this.#leaseTtlMs).catch(() => ({ acquired: false }))).acquired) return;
69
+ let hasLease = true;
70
+ const renewalTimer = setInterval(() => {
71
+ this.#leaseProvider.renewLease(LEASE_KEY, this.#leaseOwner, this.#leaseTtlMs).then((renewed) => {
72
+ if (!renewed) {
73
+ hasLease = false;
74
+ clearInterval(renewalTimer);
75
+ this.deps?.logger.warn("GitHub reconcile lease was lost mid-sweep; aborting further writes");
76
+ }
77
+ }).catch((error) => {
78
+ hasLease = false;
79
+ clearInterval(renewalTimer);
80
+ this.deps?.logger.warn("GitHub reconcile lease renewal failed; aborting further writes", { error: error instanceof Error ? error.message : String(error) });
81
+ });
82
+ }, Math.max(1e3, Math.floor(this.#leaseTtlMs / 3)));
83
+ renewalTimer.unref?.();
67
84
  try {
68
85
  const targets = await this.#targets();
69
86
  if (targets.length === 0) return;
@@ -80,10 +97,30 @@ var GithubReconcileWorker = class extends MastraWorker {
80
97
  });
81
98
  else if (counts.merged > 0 || counts.closed > 0) this.deps?.logger.info("GitHub pull request reconcile replayed missed merges/closes", context);
82
99
  else this.deps?.logger.debug("GitHub pull request reconcile sweep completed", context);
100
+ if (this.#reconcileIssues && hasLease) {
101
+ const issueStartedAt = Date.now();
102
+ try {
103
+ const { errors: issueErrors, ...issueCounts } = await this.#reconcileIssues(targets);
104
+ const issueContext = {
105
+ ...issueCounts,
106
+ candidateRepositories: targets.length,
107
+ durationMs: Date.now() - issueStartedAt
108
+ };
109
+ if (issueCounts.failed > 0) this.deps?.logger.warn("GitHub issue reconcile sweep completed with failures", {
110
+ ...issueContext,
111
+ errors: issueErrors
112
+ });
113
+ else if (issueCounts.updated > 0) this.deps?.logger.info("GitHub issue reconcile patched stale metadata", issueContext);
114
+ else this.deps?.logger.debug("GitHub issue reconcile sweep completed", issueContext);
115
+ } catch (issueError) {
116
+ this.deps?.logger.warn("GitHub issue reconcile sweep failed", { error: issueError instanceof Error ? issueError.message : String(issueError) });
117
+ }
118
+ } else if (this.#reconcileIssues && !hasLease) this.deps?.logger.debug("GitHub issue reconcile skipped: lease lost during pull-request sweep");
83
119
  } catch (error) {
84
120
  this.deps?.logger.warn("GitHub pull request reconcile sweep failed", { error: error instanceof Error ? error.message : String(error) });
85
121
  } finally {
86
- await this.#leaseProvider.releaseLease(LEASE_KEY, this.#leaseOwner).catch(() => void 0);
122
+ clearInterval(renewalTimer);
123
+ if (hasLease) await this.#leaseProvider.releaseLease(LEASE_KEY, this.#leaseOwner).catch(() => void 0);
87
124
  }
88
125
  }
89
126
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile-worker.js","names":["#reconcile","#sourceControl","#intervalMs","#leaseTtlMs","#leaseOwner","#leaseProvider","#running","#schedule","#timer","#inFlight","#tick","#targets"],"sources":["../../../src/integrations/github/reconcile-worker.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { isLeaseProvider, NoopLeaseProvider } from '@mastra/core/events';\nimport type { LeaseProvider, PubSub } from '@mastra/core/events';\nimport { MastraWorker } from '@mastra/core/worker';\nimport type { WorkerDeps } from '@mastra/core/worker';\n\nimport type {\n ConfiguredExternalRepositoryKey,\n ExternalRepositoryProjectTarget,\n SourceControlRepository,\n} from '../../storage/domains/source-control/base.js';\nimport type { GithubPullRequestReconciler, ReconcileRepository } from './rules.js';\n\nexport const DEFAULT_GITHUB_RECONCILE_INTERVAL_MS = 5 * 60_000;\nconst MIN_LEASE_TTL_MS = 30_000;\nconst LEASE_KEY = 'github:pull-request-reconcile';\n\n/** Storage slice the sweep needs to turn configured repositories into targets. */\nexport interface GithubReconcileRepositorySource {\n projectRepositories: {\n listConfiguredExternalKeys(): Promise<ConfiguredExternalRepositoryKey[]>;\n listByExternalRepository(args: {\n installationExternalId: string;\n repositoryExternalId: string;\n }): Promise<ExternalRepositoryProjectTarget[]>;\n };\n repositories: {\n findByExternalId(args: { orgId: string; externalId: string }): Promise<SourceControlRepository | null>;\n };\n}\n\nexport interface GithubReconcileWorkerConfig {\n reconcile: GithubPullRequestReconciler;\n sourceControl: GithubReconcileRepositorySource;\n intervalMs?: number;\n}\n\n/**\n * Periodic merge-state sweep for the self-hosted GitHub integration: webhooks\n * are the only other writer of merge state, and a deployment GitHub cannot\n * reach never receives one, so cards would stay open forever.\n */\nexport class GithubReconcileWorker extends MastraWorker {\n readonly name = 'github-pull-request-reconcile';\n\n readonly #reconcile: GithubPullRequestReconciler;\n readonly #sourceControl: GithubReconcileRepositorySource;\n readonly #intervalMs: number;\n readonly #leaseTtlMs: number;\n readonly #leaseOwner = randomUUID();\n\n #running = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #inFlight: Promise<void> | undefined;\n #leaseProvider: LeaseProvider = NoopLeaseProvider;\n\n constructor(config: GithubReconcileWorkerConfig) {\n super();\n this.#reconcile = config.reconcile;\n this.#sourceControl = config.sourceControl;\n this.#intervalMs = config.intervalMs ?? DEFAULT_GITHUB_RECONCILE_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error('GitHub pull request reconcile interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n }\n\n async init(deps: WorkerDeps): Promise<void> {\n await super.init(deps);\n this.#leaseProvider = getLeaseProvider(deps.pubsub);\n }\n\n async start(): Promise<void> {\n if (this.#running) return;\n if (!this.deps) throw new Error('GithubReconcileWorker: call init() before start()');\n this.#running = true;\n this.deps.logger.info('GitHub pull request reconcile worker started', { intervalMs: this.#intervalMs });\n // Sweep on boot: a restart is exactly when webhooks were most likely missed.\n this.#schedule(0);\n }\n\n async stop(): Promise<void> {\n if (!this.#running) return;\n this.#running = false;\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = undefined;\n await this.#inFlight;\n }\n\n get isRunning(): boolean {\n return this.#running;\n }\n\n #schedule(delayMs: number): void {\n if (!this.#running) return;\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n const run = this.#tick().finally(() => {\n this.#inFlight = undefined;\n this.#schedule(this.#intervalMs);\n });\n this.#inFlight = run;\n }, delayMs);\n this.#timer.unref?.();\n }\n\n async #tick(): Promise<void> {\n // Replicas share one sweep: the ingress dedupes duplicate writes, but the\n // reads still burn the installation's rate limit.\n const lease = await this.#leaseProvider\n .acquireLease(LEASE_KEY, this.#leaseOwner, this.#leaseTtlMs)\n .catch(() => ({ acquired: false }));\n if (!lease.acquired) return;\n\n try {\n const targets = await this.#targets();\n if (targets.length === 0) return;\n const startedAt = Date.now();\n const { errors, ...counts } = await this.#reconcile(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('GitHub pull request reconcile sweep completed with failures', { ...context, errors });\n } else if (counts.merged > 0 || counts.closed > 0) {\n this.deps?.logger.info('GitHub pull request reconcile replayed missed merges/closes', context);\n } else {\n this.deps?.logger.debug('GitHub pull request reconcile sweep completed', context);\n }\n } catch (error) {\n this.deps?.logger.warn('GitHub pull request reconcile sweep failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n } finally {\n await this.#leaseProvider.releaseLease(LEASE_KEY, this.#leaseOwner).catch(() => undefined);\n }\n }\n\n /**\n * Configured (installation, repository) pairs resolved to the numeric ids and\n * slug the sweep addresses GitHub with. A pair whose repository row is gone\n * is skipped rather than failing the sweep for the others.\n */\n async #targets(): Promise<ReconcileRepository[]> {\n const keys = await this.#sourceControl.projectRepositories.listConfiguredExternalKeys();\n const targets: ReconcileRepository[] = [];\n for (const key of keys) {\n const installationId = Number(key.installationExternalId);\n const repositoryId = Number(key.repositoryExternalId);\n if (!Number.isSafeInteger(installationId) || !Number.isSafeInteger(repositoryId)) continue;\n const projects = await this.#sourceControl.projectRepositories.listByExternalRepository(key);\n const orgId = projects[0]?.orgId;\n if (!orgId) continue;\n const repository = await this.#sourceControl.repositories.findByExternalId({\n orgId,\n externalId: key.repositoryExternalId,\n });\n if (!repository?.slug) continue;\n targets.push({ id: repositoryId, fullName: repository.slug, installationId });\n }\n return targets;\n }\n}\n\nfunction getLeaseProvider(pubsub: PubSub): LeaseProvider {\n const getProvider = (pubsub as PubSub & { getLeaseProvider?: () => LeaseProvider | undefined }).getLeaseProvider;\n if (typeof getProvider === 'function') return getProvider.call(pubsub) ?? NoopLeaseProvider;\n return isLeaseProvider(pubsub) ? pubsub : NoopLeaseProvider;\n}\n"],"mappings":";;;;AAcA,MAAa,uCAAuC,IAAI;AACxD,MAAM,mBAAmB;AACzB,MAAM,YAAY;;;;;;AA2BlB,IAAa,wBAAb,cAA2C,aAAa;CACtD,OAAgB;CAEhB;CACA;CACA;CACA;CACA,cAAuB,WAAW;CAElC,WAAW;CACX;CACA;CACA,iBAAgC;CAEhC,YAAY,QAAqC;EAC/C,MAAM;EACN,KAAKA,aAAa,OAAO;EACzB,KAAKC,iBAAiB,OAAO;EAC7B,KAAKC,cAAc,OAAO,cAAA;EAC1B,IAAI,CAAC,OAAO,SAAS,KAAKA,WAAW,KAAK,KAAKA,eAAe,GAC5D,MAAM,IAAI,MAAM,mEAAmE;EAErF,KAAKC,cAAc,KAAK,IAAI,kBAAkB,KAAKD,cAAc,CAAC;CACpE;CAEA,MAAM,KAAK,MAAiC;EAC1C,MAAM,MAAM,KAAK,IAAI;EACrB,KAAKG,iBAAiB,iBAAiB,KAAK,MAAM;CACpD;CAEA,MAAM,QAAuB;EAC3B,IAAI,KAAKC,UAAU;EACnB,IAAI,CAAC,KAAK,MAAM,MAAM,IAAI,MAAM,mDAAmD;EACnF,KAAKA,WAAW;EAChB,KAAK,KAAK,OAAO,KAAK,gDAAgD,EAAE,YAAY,KAAKJ,YAAY,CAAC;EAEtG,KAAKK,UAAU,CAAC;CAClB;CAEA,MAAM,OAAsB;EAC1B,IAAI,CAAC,KAAKD,UAAU;EACpB,KAAKA,WAAW;EAChB,IAAI,KAAKE,QAAQ,aAAa,KAAKA,MAAM;EACzC,KAAKA,SAAS,KAAA;EACd,MAAM,KAAKC;CACb;CAEA,IAAI,YAAqB;EACvB,OAAO,KAAKH;CACd;CAEA,UAAU,SAAuB;EAC/B,IAAI,CAAC,KAAKA,UAAU;EACpB,KAAKE,SAAS,iBAAiB;GAC7B,KAAKA,SAAS,KAAA;GACd,MAAM,MAAM,KAAKE,MAAM,CAAC,CAAC,cAAc;IACrC,KAAKD,YAAY,KAAA;IACjB,KAAKF,UAAU,KAAKL,WAAW;GACjC,CAAC;GACD,KAAKO,YAAY;EACnB,GAAG,OAAO;EACV,KAAKD,OAAO,QAAQ;CACtB;CAEA,MAAME,QAAuB;EAM3B,IAAI,EAAC,MAHe,KAAKL,eACtB,aAAa,WAAW,KAAKD,aAAa,KAAKD,WAAW,CAAC,CAC3D,aAAa,EAAE,UAAU,MAAM,EAAE,EAAA,CACzB,UAAU;EAErB,IAAI;GACF,MAAM,UAAU,MAAM,KAAKQ,SAAS;GACpC,IAAI,QAAQ,WAAW,GAAG;GAC1B,MAAM,YAAY,KAAK,IAAI;GAC3B,MAAM,EAAE,QAAQ,GAAG,WAAW,MAAM,KAAKX,WAAW,OAAO;GAC3D,MAAM,UAAU;IAAE,GAAG;IAAQ,uBAAuB,QAAQ;IAAQ,YAAY,KAAK,IAAI,IAAI;GAAU;GACvG,IAAI,OAAO,SAAS,GAClB,KAAK,MAAM,OAAO,KAAK,+DAA+D;IAAE,GAAG;IAAS;GAAO,CAAC;QACvG,IAAI,OAAO,SAAS,KAAK,OAAO,SAAS,GAC9C,KAAK,MAAM,OAAO,KAAK,+DAA+D,OAAO;QAE7F,KAAK,MAAM,OAAO,MAAM,iDAAiD,OAAO;EAEpF,SAAS,OAAO;GACd,KAAK,MAAM,OAAO,KAAK,8CAA8C,EACnE,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAC9D,CAAC;EACH,UAAU;GACR,MAAM,KAAKK,eAAe,aAAa,WAAW,KAAKD,WAAW,CAAC,CAAC,YAAY,KAAA,CAAS;EAC3F;CACF;;;;;;CAOA,MAAMO,WAA2C;EAC/C,MAAM,OAAO,MAAM,KAAKV,eAAe,oBAAoB,2BAA2B;EACtF,MAAM,UAAiC,CAAC;EACxC,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,iBAAiB,OAAO,IAAI,sBAAsB;GACxD,MAAM,eAAe,OAAO,IAAI,oBAAoB;GACpD,IAAI,CAAC,OAAO,cAAc,cAAc,KAAK,CAAC,OAAO,cAAc,YAAY,GAAG;GAElF,MAAM,SAAQ,MADS,KAAKA,eAAe,oBAAoB,yBAAyB,GAAG,EAAA,CACpE,EAAE,EAAE;GAC3B,IAAI,CAAC,OAAO;GACZ,MAAM,aAAa,MAAM,KAAKA,eAAe,aAAa,iBAAiB;IACzE;IACA,YAAY,IAAI;GAClB,CAAC;GACD,IAAI,CAAC,YAAY,MAAM;GACvB,QAAQ,KAAK;IAAE,IAAI;IAAc,UAAU,WAAW;IAAM;GAAe,CAAC;EAC9E;EACA,OAAO;CACT;AACF;AAEA,SAAS,iBAAiB,QAA+B;CACvD,MAAM,cAAe,OAA2E;CAChG,IAAI,OAAO,gBAAgB,YAAY,OAAO,YAAY,KAAK,MAAM,KAAK;CAC1E,OAAO,gBAAgB,MAAM,IAAI,SAAS;AAC5C"}
1
+ {"version":3,"file":"reconcile-worker.js","names":["#reconcile","#reconcileIssues","#sourceControl","#intervalMs","#leaseTtlMs","#leaseOwner","#leaseProvider","#running","#schedule","#timer","#inFlight","#tick","#targets"],"sources":["../../../src/integrations/github/reconcile-worker.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { isLeaseProvider, NoopLeaseProvider } from '@mastra/core/events';\nimport type { LeaseProvider, PubSub } from '@mastra/core/events';\nimport { MastraWorker } from '@mastra/core/worker';\nimport type { WorkerDeps } from '@mastra/core/worker';\n\nimport type {\n ConfiguredExternalRepositoryKey,\n ExternalRepositoryProjectTarget,\n SourceControlRepository,\n} from '../../storage/domains/source-control/base.js';\nimport type { GithubIssueReconciler } from './issue-reconciler.js';\nimport type { GithubPullRequestReconciler, ReconcileRepository } from './rules.js';\n\nexport const DEFAULT_GITHUB_RECONCILE_INTERVAL_MS = 5 * 60_000;\nconst MIN_LEASE_TTL_MS = 30_000;\nconst LEASE_KEY = 'github:pull-request-reconcile';\n\n/** Storage slice the sweep needs to turn configured repositories into targets. */\nexport interface GithubReconcileRepositorySource {\n projectRepositories: {\n listConfiguredExternalKeys(): Promise<ConfiguredExternalRepositoryKey[]>;\n listByExternalRepository(args: {\n installationExternalId: string;\n repositoryExternalId: string;\n }): Promise<ExternalRepositoryProjectTarget[]>;\n };\n repositories: {\n findByExternalId(args: { orgId: string; externalId: string }): Promise<SourceControlRepository | null>;\n };\n}\n\nexport interface GithubReconcileWorkerConfig {\n reconcile: GithubPullRequestReconciler;\n /**\n * Optional issue-metadata sweep. When provided, the worker folds it into the\n * same tick as the PR reconciler, sharing the lease and the same\n * `ReconcileRepository` target discovery. Mirrors how webhook-driven ingress\n * flows both issues and PRs through one path.\n */\n reconcileIssues?: GithubIssueReconciler;\n sourceControl: GithubReconcileRepositorySource;\n intervalMs?: number;\n}\n\n/**\n * Periodic merge-state sweep for the self-hosted GitHub integration: webhooks\n * are the only other writer of merge state, and a deployment GitHub cannot\n * reach never receives one, so cards would stay open forever.\n */\nexport class GithubReconcileWorker extends MastraWorker {\n readonly name = 'github-pull-request-reconcile';\n\n readonly #reconcile: GithubPullRequestReconciler;\n readonly #reconcileIssues: GithubIssueReconciler | undefined;\n readonly #sourceControl: GithubReconcileRepositorySource;\n readonly #intervalMs: number;\n readonly #leaseTtlMs: number;\n readonly #leaseOwner = randomUUID();\n\n #running = false;\n #timer: ReturnType<typeof setTimeout> | undefined;\n #inFlight: Promise<void> | undefined;\n #leaseProvider: LeaseProvider = NoopLeaseProvider;\n\n constructor(config: GithubReconcileWorkerConfig) {\n super();\n this.#reconcile = config.reconcile;\n this.#reconcileIssues = config.reconcileIssues;\n this.#sourceControl = config.sourceControl;\n this.#intervalMs = config.intervalMs ?? DEFAULT_GITHUB_RECONCILE_INTERVAL_MS;\n if (!Number.isFinite(this.#intervalMs) || this.#intervalMs <= 0) {\n throw new Error('GitHub pull request reconcile interval must be a positive number.');\n }\n this.#leaseTtlMs = Math.max(MIN_LEASE_TTL_MS, this.#intervalMs * 3);\n }\n\n async init(deps: WorkerDeps): Promise<void> {\n await super.init(deps);\n this.#leaseProvider = getLeaseProvider(deps.pubsub);\n }\n\n async start(): Promise<void> {\n if (this.#running) return;\n if (!this.deps) throw new Error('GithubReconcileWorker: call init() before start()');\n this.#running = true;\n this.deps.logger.info('GitHub pull request reconcile worker started', { intervalMs: this.#intervalMs });\n // Sweep on boot: a restart is exactly when webhooks were most likely missed.\n this.#schedule(0);\n }\n\n async stop(): Promise<void> {\n if (!this.#running) return;\n this.#running = false;\n if (this.#timer) clearTimeout(this.#timer);\n this.#timer = undefined;\n await this.#inFlight;\n }\n\n get isRunning(): boolean {\n return this.#running;\n }\n\n #schedule(delayMs: number): void {\n if (!this.#running) return;\n this.#timer = setTimeout(() => {\n this.#timer = undefined;\n const run = this.#tick().finally(() => {\n this.#inFlight = undefined;\n this.#schedule(this.#intervalMs);\n });\n this.#inFlight = run;\n }, delayMs);\n this.#timer.unref?.();\n }\n\n async #tick(): Promise<void> {\n // Replicas share one sweep: the ingress dedupes duplicate writes, but the\n // reads still burn the installation's rate limit.\n const lease = await this.#leaseProvider\n .acquireLease(LEASE_KEY, this.#leaseOwner, this.#leaseTtlMs)\n .catch(() => ({ acquired: false }));\n if (!lease.acquired) return;\n\n // Track whether this owner still holds the lease. A `renewLease` result\n // of `false` (or a renewal error) means another replica has taken the\n // lease. Once that happens the folded issue sweep must not run — it\n // would race the new owner's writes.\n let hasLease = true;\n // Folding the issue sweep into the same tick extends how long we hold the\n // lease. Renew periodically so a replica can't grab the expired lease and\n // start an overlapping sweep partway through this one.\n const renewalTimer = setInterval(\n () => {\n void this.#leaseProvider\n .renewLease(LEASE_KEY, this.#leaseOwner, this.#leaseTtlMs)\n .then(renewed => {\n if (!renewed) {\n hasLease = false;\n clearInterval(renewalTimer);\n this.deps?.logger.warn('GitHub reconcile lease was lost mid-sweep; aborting further writes');\n }\n })\n .catch(error => {\n hasLease = false;\n clearInterval(renewalTimer);\n this.deps?.logger.warn('GitHub reconcile lease renewal failed; aborting further writes', {\n error: error instanceof Error ? error.message : String(error),\n });\n });\n },\n Math.max(1_000, Math.floor(this.#leaseTtlMs / 3)),\n );\n renewalTimer.unref?.();\n\n try {\n const targets = await this.#targets();\n if (targets.length === 0) return;\n const startedAt = Date.now();\n const { errors, ...counts } = await this.#reconcile(targets);\n const context = { ...counts, candidateRepositories: targets.length, durationMs: Date.now() - startedAt };\n if (counts.failed > 0) {\n this.deps?.logger.warn('GitHub pull request reconcile sweep completed with failures', { ...context, errors });\n } else if (counts.merged > 0 || counts.closed > 0) {\n this.deps?.logger.info('GitHub pull request reconcile replayed missed merges/closes', context);\n } else {\n this.deps?.logger.debug('GitHub pull request reconcile sweep completed', context);\n }\n // Fold the issue-metadata sweep into the same tick so a single lease\n // covers both writers of card state for these repositories. Skip it\n // if the lease was lost during the PR sweep — running would race a\n // replica that already re-acquired the lease.\n if (this.#reconcileIssues && hasLease) {\n const issueStartedAt = Date.now();\n try {\n const { errors: issueErrors, ...issueCounts } = await this.#reconcileIssues(targets);\n const issueContext = {\n ...issueCounts,\n candidateRepositories: targets.length,\n durationMs: Date.now() - issueStartedAt,\n };\n if (issueCounts.failed > 0) {\n this.deps?.logger.warn('GitHub issue reconcile sweep completed with failures', {\n ...issueContext,\n errors: issueErrors,\n });\n } else if (issueCounts.updated > 0) {\n this.deps?.logger.info('GitHub issue reconcile patched stale metadata', issueContext);\n } else {\n this.deps?.logger.debug('GitHub issue reconcile sweep completed', issueContext);\n }\n } catch (issueError) {\n this.deps?.logger.warn('GitHub issue reconcile sweep failed', {\n error: issueError instanceof Error ? issueError.message : String(issueError),\n });\n }\n } else if (this.#reconcileIssues && !hasLease) {\n this.deps?.logger.debug('GitHub issue reconcile skipped: lease lost during pull-request sweep');\n }\n } catch (error) {\n this.deps?.logger.warn('GitHub pull request reconcile sweep failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n } finally {\n clearInterval(renewalTimer);\n if (hasLease) {\n await this.#leaseProvider.releaseLease(LEASE_KEY, this.#leaseOwner).catch(() => undefined);\n }\n }\n }\n\n /**\n * Configured (installation, repository) pairs resolved to the numeric ids and\n * slug the sweep addresses GitHub with. A pair whose repository row is gone\n * is skipped rather than failing the sweep for the others.\n */\n async #targets(): Promise<ReconcileRepository[]> {\n const keys = await this.#sourceControl.projectRepositories.listConfiguredExternalKeys();\n const targets: ReconcileRepository[] = [];\n for (const key of keys) {\n const installationId = Number(key.installationExternalId);\n const repositoryId = Number(key.repositoryExternalId);\n if (!Number.isSafeInteger(installationId) || !Number.isSafeInteger(repositoryId)) continue;\n const projects = await this.#sourceControl.projectRepositories.listByExternalRepository(key);\n const orgId = projects[0]?.orgId;\n if (!orgId) continue;\n const repository = await this.#sourceControl.repositories.findByExternalId({\n orgId,\n externalId: key.repositoryExternalId,\n });\n if (!repository?.slug) continue;\n targets.push({ id: repositoryId, fullName: repository.slug, installationId });\n }\n return targets;\n }\n}\n\nfunction getLeaseProvider(pubsub: PubSub): LeaseProvider {\n const getProvider = (pubsub as PubSub & { getLeaseProvider?: () => LeaseProvider | undefined }).getLeaseProvider;\n if (typeof getProvider === 'function') return getProvider.call(pubsub) ?? NoopLeaseProvider;\n return isLeaseProvider(pubsub) ? pubsub : NoopLeaseProvider;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,KAAK,EAAuD,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAChH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AASrF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAyC1D,OAAO,KAAK,EAAE,yBAAyB,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAgB/G,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,IAAI,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,KAAK,EAAE,YAAY,CAAC;IACpB,yEAAyE;IACzE,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC7C,iEAAiE;IACjE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,yBAAyB,KAAK,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAC3F,6EAA6E;IAC7E,SAAS,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,+FAA+F;IAC/F,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE;AAuPD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,QAAQ,EAAE,CA8pB/E"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../../src/integrations/github/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,KAAK,EAAuD,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAEhH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AASrF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AA4C1D,OAAO,KAAK,EAAE,yBAAyB,EAAE,0BAA0B,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAgB/G,MAAM,WAAW,wBAAwB;IACvC,4EAA4E;IAC5E,IAAI,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,KAAK,EAAE,YAAY,CAAC;IACpB,yEAAyE;IACzE,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4FAA4F;IAC5F,UAAU,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC7C,iEAAiE;IACjE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,yBAAyB,KAAK,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAC3F,6EAA6E;IAC7E,SAAS,CAAC,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IACjC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,+FAA+F;IAC/F,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACvE;AAuPD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,QAAQ,EAAE,CAsqB/E"}
@@ -2,11 +2,13 @@ import { clearGithubPat, getGithubPat, getGithubPatStatus, setGithubPat } from "
2
2
  import { listPullRequestSubscriptionsForThread, subscribeToPullRequest } from "./subscriptions.js";
3
3
  import { SandboxBudgetError } from "../../sandbox/fleet.js";
4
4
  import { MaterializeError, WorktreeError, commitAll, computeWorktreePath, ensureProjectSandbox, isValidGitRef as isValidGitRef$1, materializeRepo, pushBranch, teardownProjectSandbox } from "./sandbox.js";
5
- import { cleanReleasedSandbox } from "./sandbox-release.js";
5
+ import { reclaimDeletedSessionSandbox } from "./sandbox-release.js";
6
+ import { resolveFactoryDefaultModelId } from "../../session/factory-session.js";
6
7
  import { getGithubFeatureDiagnostics, isGithubFeatureEnabled } from "./config.js";
7
8
  import { handleGithubWebhook } from "./webhook.js";
8
9
  import { registerApiRoute } from "@mastra/core/server";
9
10
  import { randomUUID } from "crypto";
11
+ import { UniqueViolationError } from "@mastra/core/storage";
10
12
  import { streamSSE } from "hono/streaming";
11
13
  //#region src/integrations/github/routes.ts
12
14
  /**
@@ -22,6 +24,9 @@ import { streamSSE } from "hono/streaming";
22
24
  * so the SPA can cleanly hide all GitHub UI.
23
25
  */
24
26
  const sessionOperationLocks = /* @__PURE__ */ new Map();
27
+ const MAX_SESSION_TITLE_LENGTH = 80;
28
+ const USER_SESSION_BRANCH_PREFIX = "user/session-";
29
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
25
30
  /**
26
31
  * Serialize same-session mutations within one Factory process. Factory sessions
27
32
  * normally issue these operations sequentially, so this lock is probably not
@@ -41,19 +46,6 @@ function withSessionOperationLock(sessionId, fn) {
41
46
  function loose(c) {
42
47
  return c;
43
48
  }
44
- /**
45
- * Resolve the Factory project's default model for a triage run. Best-effort:
46
- * a missing project or an uninitialized storage domain simply means "no
47
- * default", never a failed run.
48
- */
49
- async function resolveFactoryDefaultModelId(projects, factoryProjectId) {
50
- if (!projects || !factoryProjectId) return void 0;
51
- try {
52
- return (await projects.getById({ id: factoryProjectId }))?.defaultModelId ?? void 0;
53
- } catch {
54
- return;
55
- }
56
- }
57
49
  function pullRequestNumberFromUrl(value, expectedRepo) {
58
50
  try {
59
51
  const url = new URL(value);
@@ -83,6 +75,12 @@ function isCanonicalGithubIssueUrl(value, repoFullName, issueNumber) {
83
75
  function isValidGitRef(value) {
84
76
  return typeof value === "string" && value.length > 0 && value.length <= 255 && /^[A-Za-z0-9_./-]+$/.test(value);
85
77
  }
78
+ function isJsonObject(value) {
79
+ return typeof value === "object" && value !== null && !Array.isArray(value);
80
+ }
81
+ function normalizeSessionTitle(title) {
82
+ return [...title.replace(/\s+/g, " ").trim()].slice(0, MAX_SESSION_TITLE_LENGTH).join("").trimEnd() || null;
83
+ }
86
84
  /**
87
85
  * Resolve the org-scoped tenant for a GitHub request. GitHub project features
88
86
  * are org-owned, so they require both a signed-in user and a WorkOS
@@ -166,6 +164,7 @@ async function resolveProjectRepository(args) {
166
164
  }
167
165
  function polledIssueEvent(project, issue) {
168
166
  const repositoryId = Number(project.repository.externalId);
167
+ const assigneeLogins = issue.assignees ?? (issue.assignee ? [issue.assignee] : []);
169
168
  return {
170
169
  event: "issues",
171
170
  deliveryId: `poll:${repositoryId}:issue:${issue.number}:${issue.createdAt}`,
@@ -182,6 +181,7 @@ function polledIssueEvent(project, issue) {
182
181
  title: issue.title,
183
182
  html_url: issue.url,
184
183
  created_at: issue.createdAt,
184
+ assignees: assigneeLogins.map((login) => ({ login })),
185
185
  labels: issue.labels.map((name) => ({ name }))
186
186
  }
187
187
  }
@@ -207,6 +207,8 @@ function polledPullRequestEvent(project, pullRequest) {
207
207
  created_at: pullRequest.createdAt,
208
208
  state: "open",
209
209
  merged: false,
210
+ assignees: pullRequest.assignees.map((login) => ({ login })),
211
+ requested_reviewers: pullRequest.requestedReviewers.map((login) => ({ login })),
210
212
  head: { ref: pullRequest.headBranch },
211
213
  base: { ref: pullRequest.baseBranch }
212
214
  }
@@ -287,10 +289,12 @@ function buildGithubRoutes(options) {
287
289
  const runBoardIssueTriage = runIssueTriage ? async (input) => {
288
290
  if (!input.resourceId || !input.projectPath) throw new Error("GitHub issue triage requires an explicit Factory project repository");
289
291
  await github.addIssueLabels(input.installationId, input.repository, input.issueNumber, ["auto-triaged"]);
292
+ if (input.labels.includes("status: needs triage")) await github.removeIssueLabel(input.installationId, input.repository, input.issueNumber, "status: needs triage");
293
+ const labels = input.labels.filter((label) => label !== "status: needs triage");
290
294
  return runIssueTriage({
291
295
  ...input,
292
296
  defaultModelId: input.defaultModelId ?? await resolveFactoryDefaultModelId(options.projects, input.resourceId),
293
- labels: input.labels.includes("auto-triaged") ? input.labels : [...input.labels, "auto-triaged"]
297
+ labels: labels.includes("auto-triaged") ? labels : [...labels, "auto-triaged"]
294
298
  });
295
299
  } : void 0;
296
300
  routes.push(registerApiRoute("/web/github/subscriptions", {
@@ -538,6 +542,8 @@ function buildGithubRoutes(options) {
538
542
  title: issue.title,
539
543
  url: issue.url,
540
544
  author: issue.author,
545
+ assignee: issue.assignee,
546
+ assignees: issue.assignees,
541
547
  labels: issue.labels,
542
548
  comments: issue.commentCount ?? 0,
543
549
  createdAt: issue.createdAt,
@@ -645,6 +651,8 @@ function buildGithubRoutes(options) {
645
651
  title: pr.title,
646
652
  url: pr.url,
647
653
  author: pr.author,
654
+ assignees: pr.assignees ?? [],
655
+ requestedReviewers: pr.requestedReviewers ?? [],
648
656
  baseBranch: pr.baseBranch,
649
657
  headBranch: pr.headBranch,
650
658
  createdAt: pr.createdAt,
@@ -999,17 +1007,44 @@ function buildProjectGitRoutes({ github, auth, fleet, emitAudit }) {
999
1007
  } catch {
1000
1008
  return c.json({ error: "Invalid JSON body" }, 400);
1001
1009
  }
1002
- if (!isValidGitRef$1(body.branch)) return c.json({ error: "Invalid branch" }, 400);
1003
- const baseBranch = body.baseBranch === void 0 ? project.defaultBranch : body.baseBranch;
1010
+ if (!isJsonObject(body)) return c.json({ error: "Invalid JSON body" }, 400);
1011
+ const requestedBaseBranch = body.baseBranch;
1012
+ if (requestedBaseBranch !== void 0 && typeof requestedBaseBranch !== "string") return c.json({ error: "Invalid baseBranch" }, 400);
1013
+ const baseBranch = requestedBaseBranch ?? project.defaultBranch;
1004
1014
  if (!isValidGitRef$1(baseBranch)) return c.json({ error: "Invalid baseBranch" }, 400);
1015
+ const requestedSessionId = body.sessionId;
1016
+ if (requestedSessionId !== void 0 && (typeof requestedSessionId !== "string" || !UUID_PATTERN.test(requestedSessionId))) return c.json({ error: "Invalid sessionId" }, 400);
1017
+ const sessionId = requestedSessionId ?? randomUUID();
1018
+ const requestedTitle = body.title;
1019
+ if (requestedTitle !== void 0 && typeof requestedTitle !== "string") return c.json({ error: "Invalid title" }, 400);
1020
+ const normalizedTitle = requestedTitle === void 0 ? null : normalizeSessionTitle(requestedTitle);
1021
+ const requestedBranch = body.branch;
1022
+ let branch;
1023
+ if (requestedBranch === void 0) branch = `${USER_SESSION_BRANCH_PREFIX}${sessionId}`;
1024
+ else if (typeof requestedBranch === "string" && isValidGitRef$1(requestedBranch)) branch = requestedBranch;
1025
+ else return c.json({ error: "Invalid branch" }, 400);
1026
+ if (requestedSessionId !== void 0) {
1027
+ const existing = await github.sourceControlStorage.sessions.getBySessionId(sessionId);
1028
+ if (existing) {
1029
+ if (existing.projectRepositoryId !== project.id || existing.orgId !== orgId || existing.userId !== userId || existing.branch !== branch) return c.json({ error: "Session ID conflict" }, 409);
1030
+ return c.json({ session: existing });
1031
+ }
1032
+ }
1005
1033
  const session = await github.sourceControlStorage.sessions.create({
1006
- sessionId: randomUUID(),
1034
+ sessionId,
1007
1035
  projectRepositoryId: project.id,
1008
1036
  orgId,
1009
1037
  userId,
1010
- branch: body.branch,
1011
- baseBranch
1038
+ branch,
1039
+ baseBranch,
1040
+ title: normalizedTitle
1041
+ }).catch(async (error) => {
1042
+ if (!(error instanceof UniqueViolationError) || requestedSessionId === void 0) throw error;
1043
+ const conflict = await github.sourceControlStorage.sessions.getBySessionId(sessionId);
1044
+ if (!conflict) throw error;
1045
+ return conflict;
1012
1046
  });
1047
+ if (requestedSessionId !== void 0 && (session.sessionId !== sessionId || session.projectRepositoryId !== project.id || session.orgId !== orgId || session.userId !== userId || session.branch !== branch)) return c.json({ error: "Session ID conflict" }, 409);
1013
1048
  return c.json({ session });
1014
1049
  }
1015
1050
  }),
@@ -1032,40 +1067,18 @@ function buildProjectGitRoutes({ github, auth, fleet, emitAudit }) {
1032
1067
  if ("response" in resolved) return resolved.response;
1033
1068
  const session = await github.sourceControlStorage.sessions.getBySessionId(c.req.param("sessionId"));
1034
1069
  if (!session || session.orgId !== resolved.tenant.orgId || session.userId !== resolved.tenant.userId) return c.json({ error: "Session not found" }, 404);
1035
- if (session.sandboxId && fleet.provider !== "local" && session.sandboxWorkdir) {
1036
- await cleanReleasedSandbox({
1037
- fleet,
1038
- sourceControl: github.sourceControlStorage,
1039
- orgId: session.orgId,
1040
- projectRepositoryId: session.projectRepositoryId,
1041
- sandboxId: session.sandboxId,
1042
- sandboxWorkdir: session.sandboxWorkdir
1043
- });
1044
- await github.sourceControlStorage.sandboxPool.release({
1045
- orgId: session.orgId,
1046
- projectRepositoryId: session.projectRepositoryId,
1047
- userId: session.userId,
1070
+ await github.sourceControlStorage.sessions.delete(session.id);
1071
+ reclaimDeletedSessionSandbox({
1072
+ fleet,
1073
+ sourceControl: github.sourceControlStorage,
1074
+ session
1075
+ }).catch((error) => {
1076
+ console.error("[GitHub Sessions] Failed to reclaim sandbox for deleted session", {
1077
+ sessionId: session.sessionId,
1048
1078
  sandboxId: session.sandboxId,
1049
- sandboxWorkdir: session.sandboxWorkdir
1079
+ error
1050
1080
  });
1051
- } else if (session.sandboxId) {
1052
- let sandbox;
1053
- try {
1054
- sandbox = await fleet.reattachSandbox(session.sandboxId);
1055
- } catch {}
1056
- await fleet.teardownSandbox({
1057
- sandboxId: session.sandboxId,
1058
- setSandboxId: async () => {},
1059
- clear: async () => {
1060
- await github.sourceControlStorage.sessions.setSandbox({
1061
- id: session.id,
1062
- sandboxId: null,
1063
- sandboxWorkdir: session.sandboxWorkdir ?? ""
1064
- });
1065
- }
1066
- }, sandbox);
1067
- }
1068
- await github.sourceControlStorage.sessions.delete(session.id);
1081
+ });
1069
1082
  return c.json({ removed: true });
1070
1083
  }
1071
1084
  }),