@dbx-tools/appkit-mastra 0.1.40 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -9,13 +9,13 @@
9
9
  }
10
10
  },
11
11
  "name": "@dbx-tools/appkit-mastra",
12
- "version": "0.1.40",
12
+ "version": "0.1.41",
13
13
  "dependencies": {
14
14
  "@databricks/sdk-experimental": "^0.17",
15
- "@dbx-tools/appkit-mastra-shared": "0.1.40",
16
- "@dbx-tools/genie": "0.1.40",
17
- "@dbx-tools/genie-shared": "0.1.40",
18
- "@dbx-tools/shared": "0.1.40",
15
+ "@dbx-tools/appkit-mastra-shared": "0.1.41",
16
+ "@dbx-tools/genie": "0.1.41",
17
+ "@dbx-tools/genie-shared": "0.1.41",
18
+ "@dbx-tools/shared": "0.1.41",
19
19
  "@mastra/ai-sdk": "^1",
20
20
  "@mastra/core": "^1",
21
21
  "@mastra/express": "^1",
@@ -25,6 +25,7 @@
25
25
  "@mastra/otel-bridge": "^1",
26
26
  "@mastra/pg": "^1",
27
27
  "fuse.js": "^7.0.0",
28
+ "pg": "^8.20.0",
28
29
  "zod": "^4.3.6"
29
30
  },
30
31
  "devDependencies": {
package/src/memory.ts CHANGED
@@ -27,23 +27,46 @@
27
27
  * is registered); per-agent settings cascade on top of that.
28
28
  */
29
29
 
30
- import { lakebase } from "@databricks/appkit";
31
- import { appkitUtils, logUtils } from "@dbx-tools/shared";
30
+ import { getUsernameWithApiLookup } from "@databricks/appkit";
31
+ import { logUtils } from "@dbx-tools/shared";
32
32
  import { fastembed } from "@mastra/fastembed";
33
33
  import { Memory } from "@mastra/memory";
34
34
  import { PgVector, PostgresStore } from "@mastra/pg";
35
35
  import { randomUUID } from "node:crypto";
36
- import type { Pool } from "pg";
36
+ import { Pool, type PoolConfig } from "pg";
37
37
 
38
38
  import type { MastraAgentDefinition, MastraMemoryConfigOverride } from "./agents.js";
39
39
  import type { MastraPluginConfig } from "./config.js";
40
40
 
41
41
  const log = logUtils.logger("mastra/memory");
42
42
 
43
- /** Pool handle returned by the AppKit `lakebase` plugin `exports().pool`. */
44
- export type LakebasePool = ReturnType<
45
- InstanceType<ReturnType<typeof lakebase>["plugin"]>["exports"]
46
- >["pool"];
43
+ /**
44
+ * Build a dedicated **service-principal** Lakebase pool for Mastra
45
+ * memory from the lakebase plugin's resolved SP pg config.
46
+ *
47
+ * The plugin's `exports().pool` is a `RoutingPool` that switches to
48
+ * the per-user (OBO) pool whenever a query runs inside an `asUser`
49
+ * scope - exactly the context the mastra plugin establishes around
50
+ * every chat turn. Memory (threads / messages + semantic recall) must
51
+ * instead always act as the app service principal: it owns the
52
+ * auto-created `mastra_*` schemas (a per-user role usually can't
53
+ * `CREATE SCHEMA`) and is shared across users, so it cannot inherit a
54
+ * request's OBO identity.
55
+ *
56
+ * `pgConfig` must be the plugin's `exports().getPgConfig()` evaluated
57
+ * **outside** any `asUser` scope (i.e. during setup), so it carries
58
+ * the SP connection target, OAuth token-refresh `password` callback,
59
+ * and any `lakebase({ pool })` tuning overrides - all of which this
60
+ * pool inherits. See the call site in `plugin.ts`.
61
+ */
62
+ export async function createServicePrincipalPool(pgConfig: PoolConfig): Promise<Pool> {
63
+ // `getPgConfig()` resolves the SP username synchronously from
64
+ // `PGUSER` / `DATABRICKS_CLIENT_ID`; fall back to the async API
65
+ // lookup (e.g. local dev authenticating via PAT) so the pool always
66
+ // has an identity to connect with.
67
+ const user = pgConfig.user ?? (await getUsernameWithApiLookup());
68
+ return new Pool({ ...pgConfig, user });
69
+ }
47
70
 
48
71
  /** Effective per-knob setting after the plugin/agent cascade. */
49
72
  type StorageSetting = MastraAgentDefinition["storage"];
@@ -51,9 +74,9 @@ type MemorySetting = MastraAgentDefinition["memory"];
51
74
 
52
75
  /**
53
76
  * True when any plugin-level or per-agent setting could need the
54
- * Lakebase pool. Used by `plugin.ts` to gate pool acquisition; the
55
- * builder also acquires lazily so missed cases still fail with a
56
- * clear lakebase-not-registered error.
77
+ * Lakebase pool. Used by `plugin.ts` to gate creation of the
78
+ * service-principal pool and the {@link MemoryBuilder} that consumes
79
+ * it; when false neither is built.
57
80
  */
58
81
  export function needsLakebase(config: MastraPluginConfig): boolean {
59
82
  if (settingNeedsSharedPool(config.storage)) return true;
@@ -65,42 +88,29 @@ export function needsLakebase(config: MastraPluginConfig): boolean {
65
88
  }
66
89
 
67
90
  /**
68
- * Look up the `lakebase` plugin and return its managed `pg.Pool`.
69
- * Throws when the sibling plugin is not registered; enabling
70
- * `storage` / `memory` without lakebase is a wiring bug, not a runtime
71
- * condition we can recover from.
72
- */
73
- export function resolveLakebasePool(
74
- context: appkitUtils.PluginContextLike | undefined,
75
- caller: MastraPluginConfig,
76
- ): LakebasePool {
77
- return appkitUtils.require(context, lakebase, caller).exports().pool;
78
- }
79
-
80
- /**
81
- * Construct a per-agent {@link Memory} factory. Caches the shared
82
- * `PgVector` singleton (built on first need) and the lazily-resolved
83
- * Lakebase pool so each agent build is O(1) after the first.
91
+ * Construct a per-agent {@link Memory} factory bound to the supplied
92
+ * service-principal pool (see {@link createServicePrincipalPool}).
93
+ * Caches the shared `PgVector` singleton (built on first need) so each
94
+ * agent build is O(1) after the first.
84
95
  */
85
96
  export function createMemoryBuilder(
86
97
  config: MastraPluginConfig,
87
- context: appkitUtils.PluginContextLike | undefined,
98
+ servicePrincipalPool: Pool,
88
99
  ): MemoryBuilder {
89
- return new MemoryBuilder(config, context);
100
+ return new MemoryBuilder(config, servicePrincipalPool);
90
101
  }
91
102
 
92
103
  /**
93
- * Builds one `Memory` per agent. Per-instance state keeps the shared
94
- * `PgVector` and the resolved Lakebase pool alive across calls so
95
- * registering N agents stays cheap.
104
+ * Builds one `Memory` per agent against a shared service-principal
105
+ * Lakebase pool. Per-instance state keeps the shared `PgVector` alive
106
+ * across calls so registering N agents stays cheap.
96
107
  */
97
108
  export class MemoryBuilder {
98
109
  private sharedVector: PgVector | undefined;
99
- private pool: LakebasePool | undefined;
100
110
 
101
111
  constructor(
102
112
  private readonly config: MastraPluginConfig,
103
- private readonly context: appkitUtils.PluginContextLike | undefined,
113
+ private readonly servicePrincipalPool: Pool,
104
114
  ) {}
105
115
 
106
116
  /**
@@ -133,7 +143,7 @@ export class MemoryBuilder {
133
143
  return new PostgresStore({
134
144
  id: "mastra-store__instance",
135
145
  schemaName: "mastra_instance",
136
- pool: this.requirePool() as Pool,
146
+ pool: this.servicePrincipalPool,
137
147
  });
138
148
  }
139
149
 
@@ -179,7 +189,7 @@ export class MemoryBuilder {
179
189
  return new PostgresStore({
180
190
  id: `mastra-store__${agentId}`,
181
191
  schemaName: `mastra_${agentId}`,
182
- pool: this.requirePool() as Pool,
192
+ pool: this.servicePrincipalPool,
183
193
  });
184
194
  }
185
195
  // Cast: `withId` guarantees `id` is set, but the distributive
@@ -209,17 +219,10 @@ export class MemoryBuilder {
209
219
 
210
220
  private getSharedVector(): PgVector {
211
221
  if (!this.sharedVector) {
212
- this.sharedVector = buildSharedPgVector(this.requirePool());
222
+ this.sharedVector = buildSharedPgVector(this.servicePrincipalPool);
213
223
  }
214
224
  return this.sharedVector;
215
225
  }
216
-
217
- private requirePool(): LakebasePool {
218
- if (!this.pool) {
219
- this.pool = resolveLakebasePool(this.context, this.config);
220
- }
221
- return this.pool;
222
- }
223
226
  }
224
227
 
225
228
  /**
@@ -242,7 +245,7 @@ export class MemoryBuilder {
242
245
  * the lakebase pool from then on. The placeholder pool is `.end()`'d
243
246
  * so its socket book-keeping is released.
244
247
  */
245
- function buildSharedPgVector(pool: LakebasePool): PgVector {
248
+ function buildSharedPgVector(pool: Pool): PgVector {
246
249
  const vector = new PgVector({
247
250
  id: `pg${randomUUID()}`,
248
251
  host: "-1",
@@ -252,7 +255,7 @@ function buildSharedPgVector(pool: LakebasePool): PgVector {
252
255
  password: "_",
253
256
  });
254
257
  const placeholder = vector.pool;
255
- vector.pool = pool as Pool;
258
+ vector.pool = pool;
256
259
  void placeholder.end().catch(() => undefined);
257
260
  return vector;
258
261
  }
package/src/plugin.ts CHANGED
@@ -37,11 +37,12 @@ import {
37
37
  type PluginManifest,
38
38
  type ResourceRequirement,
39
39
  } from "@databricks/appkit";
40
- import { appkitUtils, logUtils } from "@dbx-tools/shared";
40
+ import { appkitUtils, commonUtils, logUtils } from "@dbx-tools/shared";
41
41
  import { chatRoute } from "@mastra/ai-sdk";
42
42
  import type { Agent } from "@mastra/core/agent";
43
43
  import { Mastra } from "@mastra/core/mastra";
44
44
  import express from "express";
45
+ import type { Pool } from "pg";
45
46
 
46
47
  import type {
47
48
  MastraClientConfig,
@@ -52,7 +53,11 @@ import { fetchChart } from "./chart.js";
52
53
  import type { MastraPluginConfig } from "./config.js";
53
54
  import { collectSpaceSuggestions, resolveGenieSpaces } from "./genie.js";
54
55
  import { historyRoute } from "./history.js";
55
- import { createMemoryBuilder, needsLakebase } from "./memory.js";
56
+ import {
57
+ createMemoryBuilder,
58
+ createServicePrincipalPool,
59
+ needsLakebase,
60
+ } from "./memory.js";
56
61
  import { buildObservability } from "./observability.js";
57
62
  import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
58
63
  import {
@@ -125,6 +130,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
125
130
  private mastra: Mastra | null = null;
126
131
  private mastraApp: express.Express | null = null;
127
132
  private mastraServer: MastraServer | null = null;
133
+ /**
134
+ * Dedicated service-principal Lakebase pool backing Mastra memory /
135
+ * storage. Built once in {@link buildAgentAndServer} (outside any
136
+ * `asUser` scope, so it never inherits a request's OBO identity) and
137
+ * drained in {@link abortActiveOperations}. `null` until setup runs
138
+ * or when Lakebase isn't needed.
139
+ */
140
+ private servicePrincipalPool: Pool | null = null;
128
141
 
129
142
  override async setup(): Promise<void> {
130
143
  // Wait until sibling plugins (e.g. `lakebase`) finish `setup()` so
@@ -150,6 +163,26 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
150
163
  if (this.config.memory === undefined) this.config.memory = true;
151
164
  }
152
165
 
166
+ /**
167
+ * Drain the memory service-principal pool on shutdown. AppKit calls
168
+ * this during teardown; the lakebase plugin closes its own SP / OBO
169
+ * pools the same way. Fire-and-forget so shutdown isn't blocked on a
170
+ * slow drain, and clear the handle so a re-`setup()` rebuilds it.
171
+ */
172
+ override abortActiveOperations(): void {
173
+ super.abortActiveOperations();
174
+ if (this.servicePrincipalPool) {
175
+ this.log.info("closing memory SP pool");
176
+ const pool = this.servicePrincipalPool;
177
+ this.servicePrincipalPool = null;
178
+ pool.end().catch((err) => {
179
+ this.log.error("error closing memory SP pool", {
180
+ error: commonUtils.errorMessage(err),
181
+ });
182
+ });
183
+ }
184
+ }
185
+
153
186
  override exports() {
154
187
  return {
155
188
  /**
@@ -451,12 +484,25 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
451
484
  }
452
485
 
453
486
  private async buildAgentAndServer(): Promise<void> {
454
- // Per-agent memory factory. The builder resolves the Lakebase pool
455
- // lazily (on first agent that actually needs storage / vector) and
456
- // caches both the pool and the shared `PgVector` singleton so
457
- // registering N agents stays cheap. See `./memory.js`.
458
- const memoryBuilder = needsLakebase(this.config)
459
- ? createMemoryBuilder(this.config, this.context)
487
+ // Per-agent memory factory. When any storage / memory setting needs
488
+ // Postgres, stand up a dedicated service-principal pool first so
489
+ // memory acts as the app SP (owner of the `mastra_*` schemas),
490
+ // never the per-request OBO identity the chat turn runs under.
491
+ // `getPgConfig()` is read here, outside any `asUser` scope, so it
492
+ // returns the SP connection target + token refresh plus any
493
+ // `lakebase({ pool })` overrides; `require` turns a missing
494
+ // sibling into a clear wiring error. The builder caches the shared
495
+ // `PgVector` singleton so registering N agents stays cheap. See
496
+ // `./memory.js`.
497
+ if (needsLakebase(this.config)) {
498
+ const spPgConfig = appkitUtils
499
+ .require(this.context, lakebase, this.config)
500
+ .exports()
501
+ .getPgConfig();
502
+ this.servicePrincipalPool = await createServicePrincipalPool(spPgConfig);
503
+ }
504
+ const memoryBuilder = this.servicePrincipalPool
505
+ ? createMemoryBuilder(this.config, this.servicePrincipalPool)
460
506
  : undefined;
461
507
 
462
508
  this.log.debug("build:start", {