@mastra/mysql 0.15.0-alpha.0 → 0.16.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -3,6 +3,7 @@ let _mastra_core_error = require("@mastra/core/error");
3
3
  let _mastra_core_storage = require("@mastra/core/storage");
4
4
  let mysql2_promise = require("mysql2/promise");
5
5
  let _mastra_core_utils = require("@mastra/core/utils");
6
+ let _mastra_core_features = require("@mastra/core/features");
6
7
  let crypto$1 = require("crypto");
7
8
  let _mastra_core_agent = require("@mastra/core/agent");
8
9
  let _mastra_core_storage_domains_skills = require("@mastra/core/storage/domains/skills");
@@ -1376,7 +1377,7 @@ function serializeJson(v) {
1376
1377
  if (typeof v === "object" && v != null) return JSON.stringify(v);
1377
1378
  return v ?? null;
1378
1379
  }
1379
- function parseJson$2(val) {
1380
+ function parseJson$1(val) {
1380
1381
  if (val == null) return void 0;
1381
1382
  if (typeof val === "string") try {
1382
1383
  return JSON.parse(val);
@@ -1391,14 +1392,14 @@ function rowToTask(row) {
1391
1392
  status: String(row.status),
1392
1393
  toolName: String(row.tool_name),
1393
1394
  toolCallId: String(row.tool_call_id),
1394
- args: parseJson$2(row.args) ?? {},
1395
+ args: parseJson$1(row.args) ?? {},
1395
1396
  agentId: String(row.agent_id),
1396
1397
  threadId: row.thread_id != null ? String(row.thread_id) : void 0,
1397
1398
  resourceId: row.resource_id != null ? String(row.resource_id) : void 0,
1398
1399
  runId: String(row.run_id),
1399
- result: parseJson$2(row.result),
1400
- error: parseJson$2(row.error),
1401
- suspendPayload: parseJson$2(row.suspend_payload),
1400
+ result: parseJson$1(row.result),
1401
+ error: parseJson$1(row.error),
1402
+ suspendPayload: parseJson$1(row.suspend_payload),
1402
1403
  retryCount: Number(row.retry_count),
1403
1404
  maxRetries: Number(row.max_retries),
1404
1405
  timeoutMs: Number(row.timeout_ms),
@@ -1409,7 +1410,7 @@ function rowToTask(row) {
1409
1410
  ownerId: row.ownerId != null ? String(row.ownerId) : void 0,
1410
1411
  leaseExpiresAt: parseDateTime(row.leaseExpiresAt),
1411
1412
  expiresAt: parseDateTime(row.expiresAt),
1412
- metadata: parseJson$2(row.metadata)
1413
+ metadata: parseJson$1(row.metadata)
1413
1414
  };
1414
1415
  }
1415
1416
  var BackgroundTasksMySQL = class BackgroundTasksMySQL extends _mastra_core_storage.BackgroundTasksStorage {
@@ -4187,899 +4188,1645 @@ var FavoritesMySQL = class FavoritesMySQL extends _mastra_core_storage.Favorites
4187
4188
  }
4188
4189
  }
4189
4190
  };
4190
- // src/storage/domains/knowledge/index.ts
4191
- let assertDescriptionWithinBound;
4192
- async function assertKnowledgeDescriptionWithinBoundCompat(description) {
4193
- let assertWithinBound = assertDescriptionWithinBound;
4194
- if (!assertWithinBound) {
4195
- const resolvedAssert = (await import("@mastra/core/storage")).assertKnowledgeDescriptionWithinBound ?? ((value) => {
4196
- if (value !== void 0 && value.length > 400) throw new Error("Knowledge node description exceeds the 400 UTF-16 code unit limit");
4197
- });
4198
- assertDescriptionWithinBound = resolvedAssert;
4199
- assertWithinBound = resolvedAssert;
4200
- }
4201
- assertWithinBound(description);
4202
- }
4203
- let assertRecordTextWithinBound;
4204
- async function assertKnowledgeRecordTextWithinBoundCompat(text) {
4205
- if (!assertRecordTextWithinBound) assertRecordTextWithinBound = (await import("@mastra/core/storage")).assertKnowledgeRecordTextWithinBound ?? ((value) => {
4206
- if (value.length > 1e3) throw new Error("Knowledge record text exceeds the 1000 UTF-16 code unit limit; split it into separate facts or summarize it");
4207
- });
4208
- assertRecordTextWithinBound(text);
4209
- }
4210
- function mysqlSql(sql) {
4211
- return sql.replaceAll("jsonb(?)", "CAST(? AS JSON)").replaceAll("INSERT OR IGNORE", "INSERT IGNORE").replaceAll("\"", "`");
4212
- }
4213
- function createExecutor(client) {
4214
- return { async execute(statement) {
4215
- const sql = typeof statement === "string" ? statement : statement.sql;
4216
- const args = (typeof statement === "string" ? [] : statement.args ?? []).map((value) => typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) ? value.replace("T", " ").replace("Z", "") : value);
4217
- const [result] = await client.query(mysqlSql(sql), args);
4218
- if (Array.isArray(result)) return {
4219
- rows: result,
4220
- rowsAffected: 0
4191
+ const KNOWLEDGE_TABLE_NAMES = [
4192
+ "mastra_knowledge_nodes",
4193
+ "mastra_knowledge_records",
4194
+ "mastra_knowledge_mentions",
4195
+ "mastra_knowledge_activity",
4196
+ "mastra_knowledge_semantic_outbox",
4197
+ "mastra_knowledge_node_scopes",
4198
+ "mastra_knowledge_record_scopes",
4199
+ "mastra_knowledge_scope_grants",
4200
+ "mastra_knowledge_access_state",
4201
+ "mastra_knowledge_scope_addresses",
4202
+ "mastra_knowledge_node_addresses",
4203
+ "mastra_knowledge_import_state",
4204
+ "mastra_knowledge_import_runs",
4205
+ "mastra_knowledge_proposals"
4206
+ ];
4207
+ /** Knowledge v1 tables with no v2 equivalent; an explicit schema reset drops them. */
4208
+ const RETIRED_KNOWLEDGE_TABLE_NAMES = ["mastra_knowledge_cursors"];
4209
+ const KNOWLEDGE_PAGES_CORE_FEATURE = "knowledge-pages";
4210
+ const TABLE_KNOWLEDGE_PAGES = "mastra_knowledge_pages";
4211
+ const TABLE_KNOWLEDGE_NOTES = "mastra_knowledge_notes";
4212
+ const TABLE_KNOWLEDGE_NOTE_STAMPS = "mastra_knowledge_note_stamps";
4213
+ /** Same name as the schema-2 table; schema 4 re-keys it to note → page. */
4214
+ const TABLE_KNOWLEDGE_PAGE_MENTIONS = "mastra_knowledge_mentions";
4215
+ const TABLE_KNOWLEDGE_PINS = "mastra_knowledge_pins";
4216
+ /** Same name as schema 2; schema 4 re-keys it to scope/page/note. */
4217
+ const TABLE_KNOWLEDGE_PAGE_ACTIVITY = "mastra_knowledge_activity";
4218
+ /** Holds `{ id: 'knowledge', schemaVersion: 4 }`, written when the schema-4 tables are created. */
4219
+ const TABLE_KNOWLEDGE_PAGES_STATE = "mastra_knowledge_access_state";
4220
+ const KNOWLEDGE_PAGES_TABLE_NAMES = [
4221
+ TABLE_KNOWLEDGE_PAGES,
4222
+ TABLE_KNOWLEDGE_NOTES,
4223
+ TABLE_KNOWLEDGE_NOTE_STAMPS,
4224
+ TABLE_KNOWLEDGE_PAGE_MENTIONS,
4225
+ TABLE_KNOWLEDGE_PINS,
4226
+ TABLE_KNOWLEDGE_PAGE_ACTIVITY,
4227
+ TABLE_KNOWLEDGE_PAGES_STATE
4228
+ ];
4229
+ /** Every Knowledge table any schema version created. Reset drops these and nothing else. */
4230
+ const KNOWLEDGE_RESETTABLE_TABLE_NAMES = [.../* @__PURE__ */ new Set([
4231
+ ...KNOWLEDGE_PAGES_TABLE_NAMES,
4232
+ ...KNOWLEDGE_TABLE_NAMES,
4233
+ ...RETIRED_KNOWLEDGE_TABLE_NAMES
4234
+ ])];
4235
+ /** Schema-1/2 tables whose presence means the store needs a reset before schema 4 can run. */
4236
+ const LEGACY_KNOWLEDGE_TABLE_NAMES = new Set([...KNOWLEDGE_TABLE_NAMES, ...RETIRED_KNOWLEDGE_TABLE_NAMES].filter((name) => !KNOWLEDGE_PAGES_TABLE_NAMES.includes(name)));
4237
+ /**
4238
+ * Classifies what an adapter found before creating schema-4 tables. `tableNames` is every
4239
+ * `mastra_knowledge_*` table present; `schemaVersion` is the state row's version, if any.
4240
+ */
4241
+ function inspectKnowledgePagesSchema(snapshot) {
4242
+ const legacy = snapshot.tableNames.filter((name) => LEGACY_KNOWLEDGE_TABLE_NAMES.has(name));
4243
+ if (legacy.length > 0) return {
4244
+ status: "incompatible-reset-required",
4245
+ schemaVersion: snapshot.schemaVersion ?? null,
4246
+ reason: `found Knowledge tables from an earlier schema (${legacy.join(", ")})`
4247
+ };
4248
+ if (snapshot.schemaVersion === 4) return {
4249
+ status: "compatible",
4250
+ schemaVersion: 4
4251
+ };
4252
+ if (snapshot.schemaVersion === void 0) {
4253
+ const present = snapshot.tableNames.filter((name) => KNOWLEDGE_PAGES_TABLE_NAMES.includes(name));
4254
+ if (present.length === 0) return {
4255
+ status: "uninitialized",
4256
+ schemaVersion: null
4221
4257
  };
4222
4258
  return {
4223
- rows: [],
4224
- rowsAffected: result.affectedRows
4259
+ status: "incompatible-reset-required",
4260
+ schemaVersion: null,
4261
+ reason: `found Knowledge tables (${present.join(", ")}) without a schema version`
4225
4262
  };
4226
- } };
4263
+ }
4264
+ return {
4265
+ status: "incompatible-reset-required",
4266
+ schemaVersion: snapshot.schemaVersion,
4267
+ reason: `Knowledge schema version ${snapshot.schemaVersion} is not 4`
4268
+ };
4227
4269
  }
4270
+ /** Schema-2 table names schema 4 reuses with a different shape. */
4271
+ const REUSED_KNOWLEDGE_TABLE_NAMES = KNOWLEDGE_TABLE_NAMES.filter((name) => KNOWLEDGE_PAGES_TABLE_NAMES.includes(name));
4228
4272
  /**
4229
- * Scope keys visible to a caller holding `scope`: a row is visible when its scope is a subset of the
4230
- * caller's (companion entries such as `resource:x:uncurated` make this more than a prefix match).
4231
- * Matching rows by `scopeKey` keeps visibility filters on an index; a caller scope has only a few
4232
- * entries, so the list stays small.
4273
+ * The old Knowledge tables an adapter drops on init, rows included: Knowledge before schema 4 was an
4274
+ * experimental store with no migration. Every schema-1/2-only table present is dropped. The table
4275
+ * names schema 4 reuses (mentions, activity, the state table) are dropped too, but only when an old
4276
+ * table is present and no schema-4 state row exists, so they are old-shaped. Schema-4-only tables
4277
+ * are never listed. On a schema-4 store this lists only tables an older process recreated during a
4278
+ * rolling deploy. Drop order is safe for adapters that enforce references.
4233
4279
  */
4234
- function visibleScopeKeys(scope) {
4235
- const canonical = (0, _mastra_core_storage.canonicalizeKnowledgeScope)(scope);
4236
- const keys = /* @__PURE__ */ new Set();
4237
- for (let mask = 1; mask < 1 << canonical.length; mask++) try {
4238
- keys.add((0, _mastra_core_storage.knowledgeScopeKey)(canonical.filter((_, index) => mask & 1 << index)));
4239
- } catch {}
4240
- return [...keys];
4280
+ function legacyKnowledgeTablesToDrop(snapshot) {
4281
+ const present = new Set(snapshot.tableNames);
4282
+ const legacy = [...LEGACY_KNOWLEDGE_TABLE_NAMES].filter((name) => present.has(name));
4283
+ if (legacy.length === 0) return [];
4284
+ const reused = snapshot.schemaVersion === 4 ? [] : REUSED_KNOWLEDGE_TABLE_NAMES.filter((name) => present.has(name));
4285
+ const drop = /* @__PURE__ */ new Set([...legacy, ...reused]);
4286
+ return [...RETIRED_KNOWLEDGE_TABLE_NAMES, ...[...KNOWLEDGE_TABLE_NAMES].reverse()].filter((name) => drop.has(name));
4241
4287
  }
4242
- const visibleSql = (keys, scopeKeyColumn = "scopeKey") => `${scopeKeyColumn} IN (${keys.map(() => "?").join(",")})`;
4243
- function parseJson$1(value) {
4244
- if (typeof value === "string") return JSON.parse(value);
4245
- if (value instanceof Uint8Array) return JSON.parse(new TextDecoder().decode(value));
4246
- if (value instanceof ArrayBuffer) return JSON.parse(new TextDecoder().decode(value));
4247
- return value;
4288
+ const KNOWLEDGE_PAGES_CORE_FUNCTIONS = [
4289
+ "assertKnowledgePageDescriptionWithinBound",
4290
+ "assertKnowledgeNoteTextWithinBound",
4291
+ "canonicalKnowledgeName",
4292
+ "knowledgePositionBetween",
4293
+ "KnowledgeMoveTooLargeError",
4294
+ "KnowledgePageDepthError",
4295
+ "KnowledgePageHasChildrenError",
4296
+ "KnowledgePageKeyConflictError"
4297
+ ];
4298
+ const KNOWLEDGE_PAGES_CORE_NUMBERS = [
4299
+ "MAX_KNOWLEDGE_MOVE_PAGES",
4300
+ "MAX_KNOWLEDGE_PAGE_DEPTH",
4301
+ "MAX_KNOWLEDGE_PAGE_LIST_LIMIT"
4302
+ ];
4303
+ function resolveKnowledgePagesCore(storageModule) {
4304
+ const module = typeof storageModule === "object" && storageModule !== null ? storageModule : {};
4305
+ const missing = [...KNOWLEDGE_PAGES_CORE_FUNCTIONS.filter((name) => typeof module[name] !== "function"), ...KNOWLEDGE_PAGES_CORE_NUMBERS.filter((name) => typeof module[name] !== "number")];
4306
+ if (missing.length > 0) throw new Error(`@mastra/core advertises Knowledge pages without the required storage API (missing ${missing.join(", ")})`);
4307
+ return module;
4248
4308
  }
4249
- function toDate(value) {
4250
- return value instanceof Date ? new Date(value) : new Date(String(value));
4309
+ /** Loads core's page-store runtime once, after checking core advertises `knowledge-pages`. */
4310
+ function createKnowledgePagesCoreLoader(features, loadStorageModule) {
4311
+ let core;
4312
+ return async () => {
4313
+ if (!features.has("knowledge-pages")) throw new Error(`Knowledge requires a @mastra/core release with the "${KNOWLEDGE_PAGES_CORE_FEATURE}" feature. Upgrade @mastra/core to use Knowledge; other storage domains keep working on this version.`);
4314
+ core ??= loadStorageModule().then(resolveKnowledgePagesCore).catch((error) => {
4315
+ core = void 0;
4316
+ throw error;
4317
+ });
4318
+ return core;
4319
+ };
4251
4320
  }
4252
- function optionalDate(value) {
4253
- return value == null ? void 0 : toDate(value);
4321
+ /**
4322
+ * Keyword-search words: the query split on whitespace, lowercased, deduplicated. Every word must
4323
+ * appear (as a case-insensitive substring) in a hit; an empty list means the search returns nothing.
4324
+ */
4325
+ function knowledgeSearchWords(query) {
4326
+ return [...new Set(query.toLowerCase().split(/\s+/).filter(Boolean))];
4254
4327
  }
4255
- function databaseTimestamp(value) {
4256
- const pad = (part, width = 2) => String(part).padStart(width, "0");
4257
- return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}.${pad(value.getMilliseconds(), 3)}`;
4328
+ /**
4329
+ * Why `key` can't be a Knowledge page key, or undefined when it can. Keys are one path segment:
4330
+ * non-empty, with no `/` (path separator), `#` (scope/path separator) or `%` (escape character).
4331
+ */
4332
+ function knowledgePageKeyProblem(key) {
4333
+ if (!key) return "Knowledge page key must not be empty";
4334
+ if (/[/#%]/.test(key)) return `Knowledge page key "${key}" must not contain "/", "#" or "%"`;
4258
4335
  }
4259
- function canonicalName(name) {
4260
- return name.trim().toLocaleLowerCase();
4336
+ // src/storage/domains/knowledge/pages.ts
4337
+ const loadPagesCore = createKnowledgePagesCoreLoader(_mastra_core_features.coreFeatures, () => import("@mastra/core/storage"));
4338
+ const assertKnowledgePageDescriptionWithinBound = async (description) => (await loadPagesCore()).assertKnowledgePageDescriptionWithinBound(description);
4339
+ const assertKnowledgeNoteTextWithinBound = async (text) => (await loadPagesCore()).assertKnowledgeNoteTextWithinBound(text);
4340
+ const T = {
4341
+ pages: "`mastra_knowledge_pages`",
4342
+ notes: "`mastra_knowledge_notes`",
4343
+ stamps: "`mastra_knowledge_note_stamps`",
4344
+ mentions: "`mastra_knowledge_mentions`",
4345
+ pins: "`mastra_knowledge_pins`",
4346
+ pinAudiences: "`mastra_knowledge_pin_audiences`",
4347
+ activity: "`mastra_knowledge_activity`"
4348
+ };
4349
+ const KEY = "`key`";
4350
+ const WHEN = "`when`";
4351
+ const BIN = "CHARACTER SET utf8mb4 COLLATE utf8mb4_bin";
4352
+ const JV = `(SELECT jv.v FROM JSON_TABLE(?, '$[*]' COLUMNS (v VARCHAR(512) ${BIN} PATH '$')) jv)`;
4353
+ /** Live sibling keys are unique per parent; NULL for deleted pages and pages without a key. */
4354
+ const LIVE_KEY_COLUMN = `VARCHAR(768) ${BIN} AS (IF(deletedAt IS NULL AND \`key\` IS NOT NULL, CONCAT(parentId, '#', \`key\`), NULL)) STORED`;
4355
+ const MISSING_PAGE_INDEX = `SELECT 1 FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'mastra_knowledge_pages' AND index_name = ?)`;
4356
+ /** Upgrades for Knowledge databases created by an earlier alpha build; `needed` returns a row when `apply` must run. */
4357
+ const KNOWLEDGE_PAGE_UPGRADES = [
4358
+ {
4359
+ needed: `SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mastra_knowledge_pages' AND column_name = 'liveKey' AND generation_expression LIKE '%scopeId%'`,
4360
+ args: [],
4361
+ apply: [`ALTER TABLE \`mastra_knowledge_pages\` DROP INDEX idx_kpages_live_key, DROP COLUMN liveKey, ADD COLUMN liveKey ${LIVE_KEY_COLUMN}, ADD UNIQUE KEY idx_kpages_live_key (liveKey)`]
4362
+ },
4363
+ {
4364
+ needed: `SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'mastra_knowledge_pin_audiences' AND column_name = 'audience' AND character_maximum_length < 512`,
4365
+ args: [],
4366
+ apply: [`ALTER TABLE \`mastra_knowledge_pin_audiences\` MODIFY audience VARCHAR(512) ${BIN} NOT NULL`]
4367
+ },
4368
+ {
4369
+ needed: MISSING_PAGE_INDEX,
4370
+ args: ["idx_kpages_path"],
4371
+ apply: ["ALTER TABLE `mastra_knowledge_pages` ADD KEY idx_kpages_path (path(255))"]
4372
+ },
4373
+ {
4374
+ needed: MISSING_PAGE_INDEX,
4375
+ args: ["idx_kpages_declaration_parent"],
4376
+ apply: ["ALTER TABLE `mastra_knowledge_pages` ADD KEY idx_kpages_declaration_parent (declaration, parentId)"]
4377
+ },
4378
+ {
4379
+ needed: `SELECT 1 FROM DUAL WHERE EXISTS (SELECT 1 FROM mastra_knowledge_pins pin, JSON_TABLE(pin.audience, '$[*]' COLUMNS (v VARCHAR(512) ${BIN} PATH '$')) jv WHERE NOT EXISTS (SELECT 1 FROM mastra_knowledge_pin_audiences pa WHERE pa.pinId = pin.id AND pa.audience = jv.v))`,
4380
+ args: [],
4381
+ apply: [`INSERT IGNORE INTO mastra_knowledge_pin_audiences (audience, pinId) SELECT jv.v, pin.id FROM mastra_knowledge_pins pin, JSON_TABLE(pin.audience, '$[*]' COLUMNS (v VARCHAR(512) ${BIN} PATH '$')) jv`]
4382
+ }
4383
+ ];
4384
+ /** DDL for the schema-4 page tables, in creation order. Every statement is idempotent. */
4385
+ const KNOWLEDGE_PAGE_DDL = [
4386
+ `CREATE TABLE IF NOT EXISTS \`mastra_knowledge_access_state\` (
4387
+ id VARCHAR(64) ${BIN} NOT NULL PRIMARY KEY,
4388
+ schemaVersion INT NOT NULL
4389
+ )`,
4390
+ `CREATE TABLE IF NOT EXISTS ${T.pages} (
4391
+ id VARCHAR(255) ${BIN} NOT NULL PRIMARY KEY,
4392
+ scopeId VARCHAR(255) ${BIN} NOT NULL,
4393
+ isScope TINYINT NOT NULL,
4394
+ address VARCHAR(512) ${BIN} NULL,
4395
+ declaration VARCHAR(255) ${BIN} NULL,
4396
+ \`key\` VARCHAR(255) ${BIN} NULL,
4397
+ parentId VARCHAR(255) ${BIN} NULL,
4398
+ position VARCHAR(255) ${BIN} NOT NULL,
4399
+ path VARCHAR(2048) ${BIN} NOT NULL,
4400
+ depth INT NOT NULL,
4401
+ name VARCHAR(512) ${BIN} NOT NULL,
4402
+ canonicalName VARCHAR(512) ${BIN} NOT NULL,
4403
+ description TEXT NULL,
4404
+ body LONGTEXT NULL,
4405
+ version INT NOT NULL,
4406
+ mergedInto VARCHAR(255) ${BIN} NULL,
4407
+ createdBy TEXT NOT NULL,
4408
+ updatedBy TEXT NOT NULL,
4409
+ createdAt VARCHAR(32) NOT NULL,
4410
+ updatedAt VARCHAR(32) NOT NULL,
4411
+ deletedAt VARCHAR(32) NULL,
4412
+ deletedBy VARCHAR(255) NULL,
4413
+ liveKey ${LIVE_KEY_COLUMN},
4414
+ UNIQUE KEY idx_kpages_address (address),
4415
+ UNIQUE KEY idx_kpages_live_key (liveKey),
4416
+ KEY idx_kpages_scope (scopeId),
4417
+ KEY idx_kpages_parent (parentId, position, id),
4418
+ KEY idx_kpages_path (path(255)),
4419
+ KEY idx_kpages_declaration_parent (declaration, parentId),
4420
+ KEY idx_kpages_canonical (canonicalName(255))
4421
+ )`,
4422
+ `CREATE TABLE IF NOT EXISTS ${T.notes} (
4423
+ id VARCHAR(255) ${BIN} NOT NULL PRIMARY KEY,
4424
+ pageId VARCHAR(255) ${BIN} NOT NULL,
4425
+ text LONGTEXT NOT NULL,
4426
+ \`when\` VARCHAR(32) NULL,
4427
+ provenance TEXT NOT NULL,
4428
+ version INT NOT NULL,
4429
+ createdAt VARCHAR(32) NOT NULL,
4430
+ updatedAt VARCHAR(32) NOT NULL,
4431
+ deletedAt VARCHAR(32) NULL,
4432
+ deletedBy VARCHAR(255) NULL,
4433
+ KEY idx_knotes_page (pageId, createdAt, id)
4434
+ )`,
4435
+ `CREATE TABLE IF NOT EXISTS ${T.stamps} (
4436
+ noteId VARCHAR(255) ${BIN} NOT NULL,
4437
+ scopeId VARCHAR(255) ${BIN} NOT NULL,
4438
+ PRIMARY KEY (noteId, scopeId),
4439
+ KEY idx_kstamps_scope (scopeId)
4440
+ )`,
4441
+ `CREATE TABLE IF NOT EXISTS ${T.mentions} (
4442
+ noteId VARCHAR(255) ${BIN} NOT NULL,
4443
+ pageId VARCHAR(255) ${BIN} NOT NULL,
4444
+ PRIMARY KEY (noteId, pageId),
4445
+ KEY idx_kmentions_page (pageId)
4446
+ )`,
4447
+ `CREATE TABLE IF NOT EXISTS ${T.pins} (
4448
+ id VARCHAR(255) ${BIN} NOT NULL PRIMARY KEY,
4449
+ pageId VARCHAR(255) ${BIN} NOT NULL,
4450
+ audience JSON NOT NULL,
4451
+ createdBy VARCHAR(255) NOT NULL,
4452
+ createdAt VARCHAR(32) NOT NULL,
4453
+ updatedAt VARCHAR(32) NOT NULL,
4454
+ UNIQUE KEY idx_kpins_page (pageId)
4455
+ )`,
4456
+ `CREATE TABLE IF NOT EXISTS ${T.pinAudiences} (
4457
+ audience VARCHAR(512) ${BIN} NOT NULL,
4458
+ pinId VARCHAR(255) ${BIN} NOT NULL,
4459
+ PRIMARY KEY (audience, pinId),
4460
+ KEY idx_kpin_audiences_pin (pinId)
4461
+ )`,
4462
+ `CREATE TABLE IF NOT EXISTS ${T.activity} (
4463
+ id VARCHAR(255) ${BIN} NOT NULL PRIMARY KEY,
4464
+ action VARCHAR(64) NOT NULL,
4465
+ scopeId VARCHAR(255) ${BIN} NOT NULL,
4466
+ pageId VARCHAR(255) ${BIN} NOT NULL,
4467
+ noteId VARCHAR(255) ${BIN} NULL,
4468
+ provenance TEXT NOT NULL,
4469
+ createdAt VARCHAR(32) NOT NULL,
4470
+ KEY idx_kactivity_page (pageId, id)
4471
+ )`
4472
+ ];
4473
+ function str(value) {
4474
+ return value === null || value === void 0 ? void 0 : String(value);
4261
4475
  }
4262
- function nodeReferenceId(node) {
4263
- return typeof node === "string" ? node : node.id;
4476
+ function date(value) {
4477
+ return value === null || value === void 0 ? void 0 : new Date(String(value));
4264
4478
  }
4265
- function escapeLikePattern(value) {
4266
- return value.replaceAll("=", "==").replaceAll("%", "=%").replaceAll("_", "=_");
4479
+ function provenance(raw) {
4480
+ return JSON.parse(String(raw));
4267
4481
  }
4268
- function parseNode(row) {
4482
+ function toPage(row) {
4269
4483
  return {
4270
4484
  id: String(row.id),
4271
- type: "node",
4485
+ scopeId: String(row.scopeId),
4486
+ parentId: str(row.parentId) ?? null,
4487
+ isScope: Number(row.isScope) === 1,
4488
+ ...row.address !== null && row.address !== void 0 ? { address: String(row.address) } : {},
4489
+ ...row.declaration !== null && row.declaration !== void 0 ? { declaration: String(row.declaration) } : {},
4490
+ ...row.key !== null && row.key !== void 0 ? { key: String(row.key) } : {},
4491
+ position: String(row.position),
4272
4492
  name: String(row.name),
4273
- kind: String(row.kind),
4274
- content: row.content == null ? void 0 : String(row.content),
4275
- description: row.description == null ? void 0 : String(row.description),
4276
- scope: parseJson$1(row.scopeJson ?? row.scope),
4493
+ ...row.description !== null && row.description !== void 0 ? { description: String(row.description) } : {},
4494
+ ...row.body !== null && row.body !== void 0 ? { body: String(row.body) } : {},
4277
4495
  version: Number(row.version),
4278
- mergedInto: row.mergedInto == null ? void 0 : String(row.mergedInto),
4279
- createdAt: toDate(row.createdAt),
4280
- updatedAt: toDate(row.updatedAt)
4496
+ ...row.mergedInto !== null && row.mergedInto !== void 0 ? { mergedInto: String(row.mergedInto) } : {},
4497
+ createdBy: provenance(row.createdBy),
4498
+ updatedBy: provenance(row.updatedBy),
4499
+ createdAt: new Date(String(row.createdAt)),
4500
+ updatedAt: new Date(String(row.updatedAt)),
4501
+ ...row.deletedAt ? { deletedAt: new Date(String(row.deletedAt)) } : {},
4502
+ ...row.deletedBy ? { deletedBy: String(row.deletedBy) } : {}
4281
4503
  };
4282
4504
  }
4283
- function parseKnowledge(row) {
4505
+ function toNote(row, stamps, mentions) {
4506
+ const when = date(row.when);
4284
4507
  return {
4285
4508
  id: String(row.id),
4286
- node: String(row.node),
4509
+ pageId: String(row.pageId),
4287
4510
  text: String(row.text),
4288
- scope: parseJson$1(row.scopeJson ?? row.scope),
4289
- sourceThreadId: String(row.sourceThreadId),
4290
- capturedAt: toDate(row.capturedAt),
4291
- when: optionalDate(row.when),
4292
- metadata: row.metadata == null ? void 0 : parseJson$1(row.metadata),
4293
- deletedAt: optionalDate(row.deletedAt),
4294
- deletedBy: row.deletedBy == null ? void 0 : String(row.deletedBy)
4511
+ ...when ? { when } : {},
4512
+ stampScopeIds: stamps,
4513
+ mentions,
4514
+ provenance: provenance(row.provenance),
4515
+ version: Number(row.version),
4516
+ createdAt: new Date(String(row.createdAt)),
4517
+ updatedAt: new Date(String(row.updatedAt)),
4518
+ ...row.deletedAt ? { deletedAt: new Date(String(row.deletedAt)) } : {},
4519
+ ...row.deletedBy ? { deletedBy: String(row.deletedBy) } : {}
4295
4520
  };
4296
4521
  }
4297
- function parseOutbox(row) {
4522
+ function toPin(row) {
4298
4523
  return {
4299
4524
  id: String(row.id),
4300
- idempotencyKey: String(row.idempotencyKey),
4301
- documentId: String(row.documentId),
4302
- documentType: String(row.documentType),
4303
- operation: String(row.operation),
4304
- scope: parseJson$1(row.scopeJson ?? row.scope),
4305
- status: String(row.status),
4306
- attempts: Number(row.attempts),
4307
- availableAt: toDate(row.availableAt),
4308
- claimedAt: optionalDate(row.claimedAt),
4309
- claimedBy: row.claimedBy == null ? void 0 : String(row.claimedBy),
4310
- createdAt: toDate(row.createdAt),
4311
- completedAt: optionalDate(row.completedAt)
4525
+ pageId: String(row.pageId),
4526
+ audience: parseJsonList(row.audience),
4527
+ createdBy: String(row.createdBy),
4528
+ createdAt: new Date(String(row.createdAt)),
4529
+ updatedAt: new Date(String(row.updatedAt))
4312
4530
  };
4313
4531
  }
4314
- const KNOWLEDGE_INDEX_DDL = [
4315
- `CREATE UNIQUE INDEX idx_knowledge_nodes_identity ON "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" (type(32), scopeKey(255), canonicalName(255))`,
4316
- `CREATE INDEX idx_knowledge_nodes_scope ON "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" (scopeKey(255), type(32))`,
4317
- `CREATE INDEX idx_knowledge_nodes_name ON "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" (type(32), canonicalName(255))`,
4318
- `CREATE INDEX idx_knowledge_records_node_latest ON "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" (node(191), id(26) DESC)`,
4319
- `CREATE INDEX idx_knowledge_records_thread_latest ON "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" (sourceThreadId(191), id(26) DESC)`,
4320
- `CREATE INDEX idx_knowledge_mentions_record ON "${_mastra_core_storage.TABLE_KNOWLEDGE_MENTIONS}" (recordId(191), sourceType(32), sourceId(191))`,
4321
- `CREATE INDEX idx_knowledge_activity_latest ON "${_mastra_core_storage.TABLE_KNOWLEDGE_ACTIVITY}" (id(26) DESC)`,
4322
- `CREATE INDEX idx_knowledge_records_scope ON "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" (scopeKey(255), id(26))`,
4323
- `CREATE INDEX idx_knowledge_activity_scope ON "${_mastra_core_storage.TABLE_KNOWLEDGE_ACTIVITY}" (scopeKey(255), id(26) DESC)`,
4324
- `CREATE UNIQUE INDEX idx_knowledge_outbox_idempotency ON "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}" (idempotencyKey(255))`,
4325
- `CREATE INDEX idx_knowledge_outbox_claim ON "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}" (status(32), availableAt, createdAt)`
4326
- ];
4327
- const KNOWLEDGE_CURATION_CURSOR_REMOVED_MESSAGE = "Knowledge curation cursors were removed: observation-time curate is the only Knowledge writer and needs no cursor.";
4328
- var KnowledgeMySQL = class extends _mastra_core_storage.KnowledgeStorage {
4329
- static getExportDDL() {
4330
- return [
4331
- generateTableSQL({
4332
- tableName: _mastra_core_storage.TABLE_KNOWLEDGE_NODES,
4333
- schema: _mastra_core_storage.KNOWLEDGE_NODES_SCHEMA
4334
- }),
4335
- generateTableSQL({
4336
- tableName: _mastra_core_storage.TABLE_KNOWLEDGE_RECORDS,
4337
- schema: _mastra_core_storage.KNOWLEDGE_RECORDS_SCHEMA
4338
- }),
4339
- generateTableSQL({
4340
- tableName: _mastra_core_storage.TABLE_KNOWLEDGE_MENTIONS,
4341
- schema: _mastra_core_storage.KNOWLEDGE_MENTIONS_SCHEMA,
4342
- compositePrimaryKey: [
4343
- "sourceType",
4344
- "sourceId",
4345
- "recordId"
4346
- ]
4347
- }),
4348
- generateTableSQL({
4349
- tableName: _mastra_core_storage.TABLE_KNOWLEDGE_ACTIVITY,
4350
- schema: _mastra_core_storage.KNOWLEDGE_ACTIVITY_SCHEMA
4351
- }),
4352
- generateTableSQL({
4353
- tableName: _mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX,
4354
- schema: _mastra_core_storage.KNOWLEDGE_SEMANTIC_OUTBOX_SCHEMA
4355
- }),
4356
- ...KNOWLEDGE_INDEX_DDL.map(mysqlSql)
4357
- ];
4358
- }
4359
- #pool;
4360
- #client;
4361
- #operations;
4362
- constructor({ pool, operations }) {
4363
- super();
4364
- this.#pool = pool;
4365
- this.#client = createExecutor(pool);
4366
- this.#operations = operations;
4532
+ function toListedPin(row) {
4533
+ return {
4534
+ ...toPin(row),
4535
+ pageName: String(row.pageName)
4536
+ };
4537
+ }
4538
+ function limitOf(limit, max) {
4539
+ return Math.max(1, Math.min(limit ?? max, max));
4540
+ }
4541
+ function encodeCursor(value) {
4542
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
4543
+ }
4544
+ function decodeCursor(cursor, length) {
4545
+ if (cursor === void 0) return void 0;
4546
+ try {
4547
+ const parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
4548
+ if (Array.isArray(parsed) && parsed.length === length && parsed.every((item) => typeof item === "string")) return parsed;
4549
+ } catch {}
4550
+ throw new Error("Invalid Knowledge cursor");
4551
+ }
4552
+ function isRetryableRace(error) {
4553
+ for (let current = error; current && typeof current === "object"; current = current.cause) {
4554
+ const { code, errno } = current;
4555
+ if (code === "ER_DUP_ENTRY" || errno === 1062 || code === "ER_LOCK_DEADLOCK" || errno === 1213) return true;
4367
4556
  }
4368
- async init() {
4369
- await this.#operations.createTable({
4370
- tableName: _mastra_core_storage.TABLE_KNOWLEDGE_NODES,
4371
- schema: _mastra_core_storage.KNOWLEDGE_NODES_SCHEMA
4372
- });
4373
- await this.#operations.alterTable({
4374
- tableName: _mastra_core_storage.TABLE_KNOWLEDGE_NODES,
4375
- schema: _mastra_core_storage.KNOWLEDGE_NODES_SCHEMA,
4376
- ifNotExists: ["description"]
4377
- });
4378
- await this.#operations.createTable({
4379
- tableName: _mastra_core_storage.TABLE_KNOWLEDGE_RECORDS,
4380
- schema: _mastra_core_storage.KNOWLEDGE_RECORDS_SCHEMA
4381
- });
4382
- await this.#operations.createTable({
4383
- tableName: _mastra_core_storage.TABLE_KNOWLEDGE_MENTIONS,
4384
- schema: _mastra_core_storage.KNOWLEDGE_MENTIONS_SCHEMA
4385
- });
4386
- await this.#operations.createTable({
4387
- tableName: _mastra_core_storage.TABLE_KNOWLEDGE_ACTIVITY,
4388
- schema: _mastra_core_storage.KNOWLEDGE_ACTIVITY_SCHEMA
4389
- });
4390
- await this.#operations.createTable({
4391
- tableName: _mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX,
4392
- schema: _mastra_core_storage.KNOWLEDGE_SEMANTIC_OUTBOX_SCHEMA
4393
- });
4394
- for (const sql of KNOWLEDGE_INDEX_DDL) try {
4395
- await this.#client.execute(sql);
4396
- } catch (error) {
4397
- if (error.code !== "ER_DUP_KEYNAME") throw error;
4557
+ return false;
4558
+ }
4559
+ function escapeLike(value) {
4560
+ return value.replace(/[=%_]/g, (match) => `=${match}`);
4561
+ }
4562
+ /**
4563
+ * A string above every string that starts with `prefix`, comparing by code point (which is how
4564
+ * `utf8mb4_bin` orders). `column >= prefix AND column < bound` is an exact, case-sensitive prefix
4565
+ * match that an index range scan can serve, where `LEFT(column, CHAR_LENGTH(prefix)) = prefix`
4566
+ * cannot. When the last code point cannot be raised (an empty prefix, or one made only of
4567
+ * U+10FFFF), the bound is the prefix plus more U+10FFFF than an address column can hold (512).
4568
+ */
4569
+ function prefixUpperBound(prefix) {
4570
+ const points = Array.from(prefix);
4571
+ while (points.length > 0) {
4572
+ const last = points.pop().codePointAt(0);
4573
+ if (last < 1114111) {
4574
+ const next = last === 55295 ? 57344 : last + 1;
4575
+ return points.join("") + String.fromCodePoint(next);
4398
4576
  }
4399
4577
  }
4400
- async dangerouslyClearAll() {
4578
+ return prefix + "􏿿".repeat(600);
4579
+ }
4580
+ /**
4581
+ * `utf8mb4_bin` is PAD SPACE: a value compares as if padded with spaces, so `p:a\t` sorts below `p:a`.
4582
+ * Starting the range at the prefix plus U+0000 keeps every value that begins with the prefix, whatever
4583
+ * code point follows. (Values equal to the prefix plus trailing spaces still match, as they always have.)
4584
+ */
4585
+ function prefixLowerBound(prefix) {
4586
+ return prefix + "\0";
4587
+ }
4588
+ /** `column` starts with `prefix`, as an index-friendly range. */
4589
+ function prefixRange(column, prefix) {
4590
+ return {
4591
+ sql: `(${column} >= ? AND ${column} < ?)`,
4592
+ args: [prefixLowerBound(prefix), prefixUpperBound(prefix)]
4593
+ };
4594
+ }
4595
+ /**
4596
+ * The readable scope page ids as a subquery. The caller's addresses, prefixes and children go in as
4597
+ * JSON arrays so a large read scope never hits a bind-parameter limit. Each kind is its own index
4598
+ * lookup (address equality, address range for a prefix, `(declaration, parentId)` for children),
4599
+ * unioned, so the cost follows the caller's reach rather than the number of scopes in the store.
4600
+ */
4601
+ function readableScopes(read) {
4602
+ if (read === "all") return {
4603
+ sql: `SELECT id FROM ${T.pages} WHERE isScope = 1`,
4604
+ args: []
4605
+ };
4606
+ const parts = [];
4607
+ const args = [];
4608
+ if (read.addresses.length > 0) {
4609
+ parts.push(`SELECT id FROM ${T.pages} WHERE isScope = 1 AND address IN ${JV}`);
4610
+ args.push(JSON.stringify(read.addresses));
4611
+ }
4612
+ if (read.prefixes.length > 0) {
4613
+ parts.push(`SELECT sp.id FROM JSON_TABLE(?, '$[*]' COLUMNS (d VARCHAR(255) ${BIN} PATH '$.declaration', lo VARCHAR(512) ${BIN} PATH '$.lo', hi VARCHAR(1200) ${BIN} PATH '$.hi')) j STRAIGHT_JOIN ${T.pages} sp USE INDEX (idx_kpages_address) ON sp.address >= j.lo AND sp.address < j.hi AND sp.isScope = 1 AND sp.declaration = j.d`);
4614
+ args.push(JSON.stringify(read.prefixes.map((entry) => ({
4615
+ declaration: entry.declaration,
4616
+ lo: prefixLowerBound(entry.prefix),
4617
+ hi: prefixUpperBound(entry.prefix)
4618
+ }))));
4619
+ }
4620
+ if (read.children.length > 0) {
4621
+ parts.push(`SELECT sp.id FROM JSON_TABLE(?, '$[*]' COLUMNS (d VARCHAR(255) ${BIN} PATH '$.declaration', pid VARCHAR(255) ${BIN} PATH '$.parentId' NULL ON EMPTY)) j STRAIGHT_JOIN ${T.pages} sp ON sp.isScope = 1 AND sp.declaration = j.d AND sp.parentId <=> j.pid`);
4622
+ args.push(JSON.stringify(read.children));
4623
+ }
4624
+ if (parts.length === 0) return {
4625
+ sql: `SELECT id FROM ${T.pages} WHERE 0`,
4626
+ args: []
4627
+ };
4628
+ return {
4629
+ sql: parts.join(" UNION "),
4630
+ args
4631
+ };
4632
+ }
4633
+ /**
4634
+ * `FROM` clause that reads the readable scopes first and then each one's pages through `idx_kpages_scope`.
4635
+ * MySQL otherwise tends to scan every page in the store and test its scope, which touches other
4636
+ * tenants' rows. The `alias` pages are in readable scopes; add the page filters in `WHERE`.
4637
+ */
4638
+ function pagesInReadableScopes(alias, read) {
4639
+ const scopes = readableScopes(read);
4640
+ return {
4641
+ sql: `FROM (${scopes.sql}) rs STRAIGHT_JOIN ${T.pages} ${alias} FORCE INDEX (idx_kpages_scope) ON ${alias}.scopeId = rs.id`,
4642
+ args: scopes.args
4643
+ };
4644
+ }
4645
+ /** `alias.scopeId` is readable. */
4646
+ function pageReadable(alias, read) {
4647
+ const scopes = readableScopes(read);
4648
+ return {
4649
+ sql: `${alias}.scopeId IN (SELECT rs.id FROM (${scopes.sql}) rs)`,
4650
+ args: scopes.args
4651
+ };
4652
+ }
4653
+ /**
4654
+ * Note `alias` is visible: its page's scope is readable, a stamp is readable, and every resolved
4655
+ * mention's page sits in a readable scope.
4656
+ */
4657
+ function noteReadable(alias, read) {
4658
+ const scopes = readableScopes(read);
4659
+ return {
4660
+ sql: `EXISTS (SELECT 1 FROM ${T.pages} vp WHERE vp.id = ${alias}.pageId AND vp.scopeId IN (${scopes.sql}))
4661
+ AND EXISTS (SELECT 1 FROM ${T.stamps} vs WHERE vs.noteId = ${alias}.id AND vs.scopeId IN (${scopes.sql}))
4662
+ AND NOT EXISTS (SELECT 1 FROM ${T.mentions} vm JOIN ${T.pages} vmp ON vmp.id = vm.pageId WHERE vm.noteId = ${alias}.id AND vmp.scopeId NOT IN (${scopes.sql}))`,
4663
+ args: [
4664
+ ...scopes.args,
4665
+ ...scopes.args,
4666
+ ...scopes.args
4667
+ ]
4668
+ };
4669
+ }
4670
+ function jsonList(values) {
4671
+ return JSON.stringify(values);
4672
+ }
4673
+ var MySQLKnowledgePages = class {
4674
+ #read;
4675
+ #transaction;
4676
+ constructor(config) {
4677
+ this.#read = config.read;
4678
+ this.#transaction = config.transaction;
4679
+ }
4680
+ /** Deletes every row from the page tables (scopes, pages, notes, pins, activity). */
4681
+ async clearAll() {
4401
4682
  await this.#transaction(async (tx) => {
4402
4683
  for (const table of [
4403
- _mastra_core_storage.TABLE_KNOWLEDGE_MENTIONS,
4404
- _mastra_core_storage.TABLE_KNOWLEDGE_RECORDS,
4405
- _mastra_core_storage.TABLE_KNOWLEDGE_NODES,
4406
- _mastra_core_storage.TABLE_KNOWLEDGE_ACTIVITY,
4407
- _mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX
4408
- ]) await tx.execute(`DELETE FROM "${table}"`);
4684
+ T.activity,
4685
+ T.pinAudiences,
4686
+ T.pins,
4687
+ T.mentions,
4688
+ T.stamps,
4689
+ T.notes,
4690
+ T.pages
4691
+ ]) await tx.execute({
4692
+ sql: `DELETE FROM ${table}`,
4693
+ args: []
4694
+ });
4409
4695
  });
4410
4696
  }
4411
- async createNode(input) {
4412
- await assertKnowledgeDescriptionWithinBoundCompat(input.description);
4413
- if (input.scopeAddresses?.length) {
4414
- const error = /* @__PURE__ */ new Error("This Knowledge storage adapter does not expose structural scope placement.");
4415
- error.name = "KnowledgeUnsupportedCapabilityError";
4416
- throw error;
4697
+ async #query(executor, sql, args = []) {
4698
+ return (await executor.execute({
4699
+ sql,
4700
+ args
4701
+ })).rows;
4702
+ }
4703
+ async #pageRow(executor, id) {
4704
+ return (await this.#query(executor, `SELECT * FROM ${T.pages} WHERE id = ?`, [id]))[0];
4705
+ }
4706
+ async #livePage(executor, id) {
4707
+ const row = await this.#pageRow(executor, id);
4708
+ if (!row || row.deletedAt) throw new _mastra_core_storage.KnowledgeNotFoundError("page", id);
4709
+ return row;
4710
+ }
4711
+ async #logActivity(tx, action, event, now) {
4712
+ await tx.execute({
4713
+ sql: `INSERT INTO ${T.activity} (id, action, scopeId, pageId, noteId, provenance, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?)`,
4714
+ args: [
4715
+ (0, _mastra_core_storage.createKnowledgeUlid)(),
4716
+ action,
4717
+ event.scopeId,
4718
+ event.pageId,
4719
+ event.noteId ?? null,
4720
+ JSON.stringify(event.provenance),
4721
+ now
4722
+ ]
4723
+ });
4724
+ }
4725
+ async #lastPosition(tx, parentId, scopesOnly) {
4726
+ const rows = await this.#query(tx, `SELECT position FROM ${T.pages} WHERE parentId <=> ? AND isScope = ? AND deletedAt IS NULL ORDER BY position DESC, id DESC LIMIT 1`, [parentId, scopesOnly ? 1 : 0]);
4727
+ return rows[0] ? String(rows[0].position) : void 0;
4728
+ }
4729
+ async materializeScope(input) {
4730
+ for (let attempt = 1;; attempt++) try {
4731
+ return await this.#materializeScopeOnce(input);
4732
+ } catch (error) {
4733
+ if (attempt >= 5 || !isRetryableRace(error)) throw error;
4417
4734
  }
4418
- const scope = (0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.scope);
4735
+ }
4736
+ async #materializeScopeOnce(input) {
4737
+ const core = await loadPagesCore();
4738
+ await assertKnowledgePageDescriptionWithinBound(input.description);
4419
4739
  return this.#transaction(async (tx) => {
4420
- const existing = await this.#getNodeByName(tx, input.name, scope);
4421
- if (existing) {
4422
- const terminal = await this.#resolveTerminalNode(tx, existing.id);
4423
- if (!(0, _mastra_core_storage.isKnowledgeScopeVisible)(terminal.scope, scope)) throw new Error(`Merged knowledge node is not visible from scope: ${input.name}`);
4424
- return terminal;
4740
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4741
+ const ensureScope = async (spec, parentId) => {
4742
+ await assertKnowledgePageDescriptionWithinBound(spec.description);
4743
+ const existing = (await this.#query(tx, `SELECT * FROM ${T.pages} WHERE isScope = 1 AND address = ?`, [spec.address]))[0];
4744
+ if (existing) return {
4745
+ row: existing,
4746
+ created: false
4747
+ };
4748
+ if (parentId !== null) await this.#livePage(tx, parentId);
4749
+ const id = (0, _mastra_core_storage.createKnowledgeUlid)();
4750
+ const position = core.knowledgePositionBetween(await this.#lastPosition(tx, parentId, true));
4751
+ const who = JSON.stringify(input.provenance);
4752
+ await tx.execute({
4753
+ sql: `INSERT INTO ${T.pages} (id, scopeId, isScope, address, declaration, parentId, position, path, depth, name, canonicalName, description, version, createdBy, updatedBy, createdAt, updatedAt) VALUES (?, ?, 1, ?, ?, ?, ?, ?, 0, ?, ?, ?, 1, ?, ?, ?, ?)`,
4754
+ args: [
4755
+ id,
4756
+ id,
4757
+ spec.address,
4758
+ spec.declaration,
4759
+ parentId,
4760
+ position,
4761
+ `/${id}/`,
4762
+ spec.name,
4763
+ core.canonicalKnowledgeName(spec.name),
4764
+ spec.description ?? null,
4765
+ who,
4766
+ who,
4767
+ now,
4768
+ now
4769
+ ]
4770
+ });
4771
+ await this.#logActivity(tx, "scope-created", {
4772
+ scopeId: id,
4773
+ pageId: id,
4774
+ provenance: input.provenance
4775
+ }, now);
4776
+ return {
4777
+ row: await this.#pageRow(tx, id),
4778
+ created: true
4779
+ };
4780
+ };
4781
+ const ensured = await ensureScope(input, input.parentId ?? null);
4782
+ let row = ensured.row;
4783
+ const created = ensured.created;
4784
+ const scopeId = String(row.id);
4785
+ if (!created && input.parentId != null && str(row.parentId) === void 0) {
4786
+ await this.#livePage(tx, input.parentId);
4787
+ if (input.parentId === scopeId) throw new Error(`Scope ${scopeId} cannot be its own parent`);
4788
+ let ancestor = input.parentId;
4789
+ while (ancestor) {
4790
+ const up = await this.#pageRow(tx, ancestor);
4791
+ if (String(up.id) === scopeId) throw new Error(`Scope ${scopeId} cannot be linked under its own descendant`);
4792
+ ancestor = str(up.parentId);
4793
+ }
4794
+ await tx.execute({
4795
+ sql: `UPDATE ${T.pages} SET parentId = ?, position = ?, version = version + 1, updatedAt = ? WHERE id = ? AND parentId IS NULL`,
4796
+ args: [
4797
+ input.parentId,
4798
+ core.knowledgePositionBetween(await this.#lastPosition(tx, input.parentId, true)),
4799
+ now,
4800
+ scopeId
4801
+ ]
4802
+ });
4803
+ row = await this.#pageRow(tx, scopeId);
4425
4804
  }
4426
- const now = /* @__PURE__ */ new Date();
4427
- const node = {
4428
- id: input.id ?? crypto.randomUUID(),
4429
- type: "node",
4430
- name: input.name.trim(),
4431
- kind: input.kind,
4432
- content: input.content,
4433
- description: input.description,
4434
- scope,
4435
- version: 1,
4436
- createdAt: now,
4437
- updatedAt: now
4805
+ let parentConflict;
4806
+ if (!created && input.parentId !== void 0 && (str(row.parentId) ?? null) !== input.parentId) parentConflict = {
4807
+ existingParentId: str(row.parentId) ?? null,
4808
+ requestedParentId: input.parentId
4809
+ };
4810
+ for (const child of input.children ?? []) await ensureScope(child, scopeId);
4811
+ const ensureTemplates = async (templates, parentId) => {
4812
+ for (const template of templates) {
4813
+ const problem = knowledgePageKeyProblem(template.key);
4814
+ if (problem) throw new Error(problem);
4815
+ const existing = (await this.#query(tx, `SELECT id FROM ${T.pages} WHERE parentId = ? AND isScope = 0 AND ${KEY} = ? AND deletedAt IS NULL`, [parentId, template.key]))[0];
4816
+ const id = existing ? String(existing.id) : (await this.#insertPage(tx, {
4817
+ parentId,
4818
+ key: template.key,
4819
+ name: template.name,
4820
+ description: template.description,
4821
+ provenance: input.provenance
4822
+ })).id;
4823
+ if (template.pages?.length) await ensureTemplates(template.pages, id);
4824
+ }
4825
+ };
4826
+ await ensureTemplates(input.pages ?? [], scopeId);
4827
+ return {
4828
+ scope: toPage(await this.#pageRow(tx, scopeId)),
4829
+ created,
4830
+ ...parentConflict ? { parentConflict } : {}
4438
4831
  };
4439
- await tx.execute({
4440
- sql: `INSERT INTO "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" (id,type,name,canonicalName,kind,content,description,scope,scopeKey,version,mergedInto,createdAt,updatedAt) VALUES (?,?,?,?,?,?,?,jsonb(?),?,?,NULL,?,?)`,
4441
- args: [
4442
- node.id,
4443
- "node",
4444
- node.name,
4445
- canonicalName(node.name),
4446
- node.kind,
4447
- node.content ?? null,
4448
- node.description ?? null,
4449
- JSON.stringify(scope),
4450
- (0, _mastra_core_storage.knowledgeScopeKey)(scope),
4451
- node.version,
4452
- now.toISOString(),
4453
- now.toISOString()
4454
- ]
4455
- });
4456
- await this.#replaceMentions(tx, "node", node.id, node.content ?? "", input.resolutionScope ?? scope, scope);
4457
- await this.#activity(tx, "node-created", "node", node.id, scope);
4458
- await this.#outbox(tx, "node", node.id, "upsert", node.version, scope);
4459
- return node;
4460
4832
  });
4461
4833
  }
4462
- async getNode(id) {
4463
- return this.#getNode(this.#client, id);
4834
+ async listScopeDeclarations() {
4835
+ return (await this.#query(this.#read, `SELECT declaration, COUNT(*) AS scopes FROM ${T.pages} WHERE isScope = 1 AND declaration IS NOT NULL GROUP BY declaration ORDER BY declaration`)).map((row) => ({
4836
+ declaration: String(row.declaration),
4837
+ scopes: Number(row.scopes)
4838
+ }));
4464
4839
  }
4465
- async getNodeByName(input) {
4466
- return this.#getNodeByName(this.#client, input.name, (0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.scope));
4840
+ async renameScopeDeclaration(input) {
4841
+ return this.#transaction(async (tx) => {
4842
+ const within = prefixRange("declaration", `${input.from}/`);
4843
+ const affected = (await this.#query(tx, `SELECT id, address, declaration FROM ${T.pages} WHERE isScope = 1 AND (declaration = ? OR ${within.sql}) FOR UPDATE`, [input.from, ...within.args])).map((row) => ({
4844
+ id: String(row.id),
4845
+ address: String(row.address),
4846
+ from: String(row.declaration)
4847
+ })).filter((row) => row.from === input.from || row.from.startsWith(`${input.from}/`)).map((row) => ({
4848
+ ...row,
4849
+ to: input.to + row.from.slice(input.from.length)
4850
+ }));
4851
+ input.check(affected.map(({ address, from, to }) => ({
4852
+ address,
4853
+ from,
4854
+ to
4855
+ })));
4856
+ for (const row of affected) await tx.execute({
4857
+ sql: `UPDATE ${T.pages} SET declaration = ? WHERE id = ?`,
4858
+ args: [row.to, row.id]
4859
+ });
4860
+ return affected.length;
4861
+ });
4467
4862
  }
4468
- async resolveNode(input) {
4469
- return this.#resolveNode(this.#client, input.name, (0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.scope));
4863
+ async getScopesByAddress(addresses) {
4864
+ if (addresses.length === 0) return [];
4865
+ return (await this.#query(this.#read, `SELECT * FROM ${T.pages} WHERE isScope = 1 AND address IN ${JV}`, [jsonList(addresses)])).map(toPage);
4470
4866
  }
4471
- async listNodes(input) {
4472
- const keys = visibleScopeKeys((0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.scope));
4473
- const clauses = [
4474
- `type = 'node'`,
4475
- "mergedInto IS NULL",
4476
- visibleSql(keys)
4477
- ];
4478
- const args = [...keys];
4479
- if (input.namePrefix) {
4480
- clauses.push("canonicalName LIKE ? ESCAPE '='");
4481
- args.push(`${escapeLikePattern(canonicalName(input.namePrefix))}%`);
4482
- }
4483
- if (input.kind) {
4484
- clauses.push("kind = ?");
4485
- args.push(input.kind);
4486
- }
4487
- if (input.hasContent !== void 0) clauses.push(input.hasContent ? "content IS NOT NULL AND content <> ''" : "(content IS NULL OR content = '')");
4488
- if (input.cursor) {
4489
- const cursor = (0, _mastra_core_storage.parseKnowledgeNodeCursor)(input.cursor, {
4490
- namePrefix: input.namePrefix,
4491
- kind: input.kind,
4492
- hasContent: input.hasContent
4493
- });
4494
- const updatedAt = databaseTimestamp(cursor.updatedAt);
4495
- clauses.push("(updatedAt < ? OR (updatedAt = ? AND (name > ? OR (name = ? AND id > ?))))");
4496
- args.push(updatedAt, updatedAt, cursor.name, cursor.name, cursor.id);
4497
- }
4498
- args.push(input.limit ?? 100);
4499
- return (await this.#client.execute({
4500
- sql: `SELECT *, scope AS scopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" WHERE ${clauses.join(" AND ")} ORDER BY updatedAt DESC, name ASC, id ASC LIMIT ?`,
4501
- args
4502
- })).rows.map(parseNode);
4867
+ async listChildScopes(input) {
4868
+ const core = await loadPagesCore();
4869
+ const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
4870
+ const cursor = decodeCursor(input.cursor, 2);
4871
+ const args = [input.parentId];
4872
+ let after = "";
4873
+ if (cursor) {
4874
+ after = ` AND (position > ? OR (position = ? AND id > ?))`;
4875
+ args.push(cursor[0], cursor[0], cursor[1]);
4876
+ }
4877
+ args.push(limit + 1);
4878
+ const rows = await this.#query(this.#read, `SELECT * FROM ${T.pages} WHERE isScope = 1 AND parentId <=> ?${after} ORDER BY position, id LIMIT ?`, args);
4879
+ return this.#pageOutput(rows, limit, (row) => [String(row.position), String(row.id)]);
4880
+ }
4881
+ #pageOutput(rows, limit, cursorOf) {
4882
+ const page = rows.slice(0, limit);
4883
+ return {
4884
+ pages: page.map(toPage),
4885
+ ...rows.length > limit ? { nextCursor: encodeCursor(cursorOf(page[page.length - 1])) } : {}
4886
+ };
4503
4887
  }
4504
- async updateNode(input) {
4505
- await assertKnowledgeDescriptionWithinBoundCompat(input.description);
4888
+ async getPage(input) {
4889
+ const readable = pageReadable("p", input.read);
4890
+ const rows = await this.#query(this.#read, `SELECT p.* FROM ${T.pages} p WHERE p.id = ? AND ${readable.sql}`, [input.id, ...readable.args]);
4891
+ return rows[0] ? toPage(rows[0]) : null;
4892
+ }
4893
+ async getPages(input) {
4894
+ const ids = [...new Set(input.ids)];
4895
+ if (ids.length === 0) return [];
4896
+ const readable = pageReadable("p", input.read);
4897
+ return (await this.#query(this.#read, `SELECT p.* FROM ${T.pages} p WHERE p.deletedAt IS NULL AND p.id IN ${JV} AND ${readable.sql}`, [jsonList(ids), ...readable.args])).map(toPage);
4898
+ }
4899
+ async getPageByPath(input) {
4900
+ const readable = pageReadable("p", input.read);
4901
+ let parentId = input.scopeId;
4902
+ let row;
4903
+ for (const key of input.path) {
4904
+ row = (await this.#query(this.#read, `SELECT p.* FROM ${T.pages} p WHERE p.parentId = ? AND p.isScope = 0 AND p.${KEY} = ? AND p.deletedAt IS NULL AND ${readable.sql}`, [
4905
+ parentId,
4906
+ key,
4907
+ ...readable.args
4908
+ ]))[0];
4909
+ if (!row) return null;
4910
+ parentId = String(row.id);
4911
+ }
4912
+ return row ? toPage(row) : null;
4913
+ }
4914
+ async findPagesByName(input) {
4915
+ const core = await loadPagesCore();
4916
+ const readable = pageReadable("p", input.read);
4917
+ const args = [core.canonicalKnowledgeName(input.name), ...readable.args];
4918
+ let scope = "";
4919
+ if (input.scopeId !== void 0) {
4920
+ scope = ` AND p.scopeId = ?`;
4921
+ args.push(input.scopeId);
4922
+ }
4923
+ args.push(limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT));
4924
+ return (await this.#query(this.#read, `SELECT p.* FROM ${T.pages} p WHERE p.canonicalName = ? AND p.deletedAt IS NULL AND ${readable.sql}${scope} ORDER BY p.createdAt DESC, p.id DESC LIMIT ?`, args)).map(toPage);
4925
+ }
4926
+ async listChildPages(input) {
4927
+ const core = await loadPagesCore();
4928
+ const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
4929
+ const cursor = decodeCursor(input.cursor, 2);
4930
+ const readable = pageReadable("p", input.read);
4931
+ const args = [input.parentId, ...readable.args];
4932
+ let after = "";
4933
+ if (cursor) {
4934
+ after = ` AND (p.position > ? OR (p.position = ? AND p.id > ?))`;
4935
+ args.push(cursor[0], cursor[0], cursor[1]);
4936
+ }
4937
+ args.push(limit + 1);
4938
+ const rows = await this.#query(this.#read, `SELECT p.* FROM ${T.pages} p WHERE p.parentId = ? AND p.isScope = 0 AND p.deletedAt IS NULL AND ${readable.sql}${after} ORDER BY p.position, p.id LIMIT ?`, args);
4939
+ return this.#pageOutput(rows, limit, (row) => [String(row.position), String(row.id)]);
4940
+ }
4941
+ async listPages(input) {
4942
+ const core = await loadPagesCore();
4943
+ const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
4944
+ const cursor = decodeCursor(input.cursor, 2);
4945
+ const readable = pagesInReadableScopes("p", input.read);
4946
+ const args = [...readable.args];
4947
+ let filters = "";
4948
+ if (input.scopeId !== void 0) {
4949
+ filters += ` AND p.scopeId = ?`;
4950
+ args.push(input.scopeId);
4951
+ }
4952
+ if (input.namePrefix) {
4953
+ const prefix = core.canonicalKnowledgeName(input.namePrefix);
4954
+ filters += ` AND LEFT(p.canonicalName, CHAR_LENGTH(?)) = ?`;
4955
+ args.push(prefix, prefix);
4956
+ }
4957
+ if (cursor) {
4958
+ filters += ` AND (p.name > ? OR (p.name = ? AND p.id > ?))`;
4959
+ args.push(cursor[0], cursor[0], cursor[1]);
4960
+ }
4961
+ args.push(limit + 1);
4962
+ const rows = await this.#query(this.#read, `SELECT p.* ${readable.sql} WHERE p.isScope = 0 AND p.deletedAt IS NULL${filters} ORDER BY p.name, p.id LIMIT ?`, args);
4963
+ return this.#pageOutput(rows, limit, (row) => [String(row.name), String(row.id)]);
4964
+ }
4965
+ async #insertPage(tx, input) {
4966
+ const core = await loadPagesCore();
4967
+ await assertKnowledgePageDescriptionWithinBound(input.description);
4968
+ const parent = await this.#livePage(tx, input.parentId);
4969
+ const isScopeParent = Number(parent.isScope) === 1;
4970
+ const scopeId = isScopeParent ? String(parent.id) : String(parent.scopeId);
4971
+ const depth = isScopeParent ? 1 : Number(parent.depth) + 1;
4972
+ if (depth > core.MAX_KNOWLEDGE_PAGE_DEPTH) throw new core.KnowledgePageDepthError(input.parentId);
4973
+ const id = input.id ?? (0, _mastra_core_storage.createKnowledgeUlid)();
4974
+ if (await this.#pageRow(tx, id)) throw new _mastra_core_storage.KnowledgeConflictError(id);
4975
+ if (input.key !== void 0) {
4976
+ const problem = knowledgePageKeyProblem(input.key);
4977
+ if (problem) throw new Error(problem);
4978
+ if ((await this.#query(tx, `SELECT id FROM ${T.pages} WHERE parentId = ? AND isScope = 0 AND ${KEY} = ? AND deletedAt IS NULL`, [String(parent.id), input.key])).length > 0) throw new core.KnowledgePageKeyConflictError(input.key, String(parent.id));
4979
+ }
4980
+ const position = input.position ?? core.knowledgePositionBetween(await this.#lastPosition(tx, String(parent.id), false));
4981
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4982
+ const who = JSON.stringify(input.provenance);
4983
+ await tx.execute({
4984
+ sql: `INSERT INTO ${T.pages} (id, scopeId, isScope, ${KEY}, parentId, position, path, depth, name, canonicalName, description, body, version, createdBy, updatedBy, createdAt, updatedAt) VALUES (?, ?, 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?)`,
4985
+ args: [
4986
+ id,
4987
+ scopeId,
4988
+ input.key ?? null,
4989
+ String(parent.id),
4990
+ position,
4991
+ `${String(parent.path)}${id}/`,
4992
+ depth,
4993
+ input.name,
4994
+ core.canonicalKnowledgeName(input.name),
4995
+ input.description || null,
4996
+ input.body ?? null,
4997
+ who,
4998
+ who,
4999
+ now,
5000
+ now
5001
+ ]
5002
+ });
5003
+ await this.#logActivity(tx, "page-created", {
5004
+ scopeId,
5005
+ pageId: id,
5006
+ provenance: input.provenance
5007
+ }, now);
5008
+ return toPage(await this.#pageRow(tx, id));
5009
+ }
5010
+ async createPage(input) {
5011
+ await assertKnowledgePageDescriptionWithinBound(input.description);
5012
+ return this.#transaction((tx) => this.#insertPage(tx, input));
5013
+ }
5014
+ async updatePage(input) {
5015
+ const core = await loadPagesCore();
5016
+ await assertKnowledgePageDescriptionWithinBound(input.description);
4506
5017
  return this.#transaction(async (tx) => {
4507
- const existing = await this.#getNode(tx, input.id);
4508
- if (!existing) throw new _mastra_core_storage.KnowledgeNotFoundError("node", input.id);
4509
- if (existing.mergedInto) throw new Error(`Cannot update merged knowledge node: ${input.id}`);
4510
- const scope = (0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.scope ?? existing.scope);
4511
- const name = (input.name ?? existing.name).trim();
4512
- const content = input.content ?? existing.content;
4513
- const description = input.description ?? existing.description;
4514
- const now = /* @__PURE__ */ new Date();
4515
- if ((await tx.execute({
4516
- sql: `UPDATE "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" SET name=?,canonicalName=?,kind=?,content=?,description=?,scope=jsonb(?),scopeKey=?,version=version+1,updatedAt=? WHERE id=? AND type='node' AND version=?`,
5018
+ const row = await this.#livePage(tx, input.id);
5019
+ if (Number(row.version) !== input.expectedVersion) throw new _mastra_core_storage.KnowledgeConflictError(input.id);
5020
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5021
+ const name = input.name ?? String(row.name);
5022
+ const description = input.description === void 0 ? row.description ?? null : input.description || null;
5023
+ const body = input.body === void 0 ? row.body ?? null : input.body;
5024
+ await tx.execute({
5025
+ sql: `UPDATE ${T.pages} SET name = ?, canonicalName = ?, description = ?, body = ?, version = version + 1, updatedBy = ?, updatedAt = ? WHERE id = ?`,
4517
5026
  args: [
4518
5027
  name,
4519
- canonicalName(name),
4520
- input.kind ?? existing.kind,
4521
- content ?? null,
4522
- description ?? null,
4523
- JSON.stringify(scope),
4524
- (0, _mastra_core_storage.knowledgeScopeKey)(scope),
4525
- now.toISOString(),
4526
- input.id,
4527
- input.version
5028
+ core.canonicalKnowledgeName(name),
5029
+ description,
5030
+ body,
5031
+ JSON.stringify(input.provenance),
5032
+ now,
5033
+ input.id
4528
5034
  ]
4529
- })).rowsAffected === 0) throw new _mastra_core_storage.KnowledgeConflictError(input.id);
4530
- if (input.content !== void 0 || input.name !== void 0 || input.scope !== void 0) await this.#replaceMentions(tx, "node", input.id, content ?? "", input.resolutionScope ?? scope, scope);
4531
- await this.#activity(tx, "node-updated", "node", input.id, scope);
4532
- if ((0, _mastra_core_storage.knowledgeScopeKey)(existing.scope) !== (0, _mastra_core_storage.knowledgeScopeKey)(scope)) {
4533
- await this.#outbox(tx, "node", input.id, "delete", (0, _mastra_core_storage.createKnowledgeUlid)(), existing.scope);
4534
- const records = await tx.execute({
4535
- sql: `SELECT id,scope AS scopeJson,deletedAt FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" WHERE node=?`,
4536
- args: [input.id]
4537
- });
4538
- for (const row of records.rows) {
4539
- const factScope = parseJson$1(row.scopeJson);
4540
- await this.#outbox(tx, "record", String(row.id), "delete", (0, _mastra_core_storage.createKnowledgeUlid)(), factScope);
4541
- if (row.deletedAt == null) await this.#outbox(tx, "record", String(row.id), "upsert", (0, _mastra_core_storage.createKnowledgeUlid)(), factScope);
4542
- }
4543
- }
4544
- await this.#outbox(tx, "node", input.id, "upsert", input.version + 1, scope);
4545
- return {
4546
- ...existing,
4547
- name,
4548
- kind: input.kind ?? existing.kind,
4549
- content,
4550
- description,
4551
- scope,
4552
- version: input.version + 1,
4553
- updatedAt: now
4554
- };
5035
+ });
5036
+ await this.#logActivity(tx, "page-updated", {
5037
+ scopeId: String(row.scopeId),
5038
+ pageId: input.id,
5039
+ provenance: input.provenance
5040
+ }, now);
5041
+ return toPage(await this.#pageRow(tx, input.id));
4555
5042
  });
4556
5043
  }
4557
- async mergeNodes(input) {
4558
- if (input.sourceId === input.targetId) throw new Error("Cannot merge a knowledge node into itself");
5044
+ /**
5045
+ * Rehomes `root` (and its subtree) under `parent`: new path prefix, scope and depth in one go.
5046
+ * Throws when the subtree is too large, too deep, or the root's key clashes with a live child of `parent`.
5047
+ */
5048
+ async #rehome(tx, root, parent, options) {
5049
+ const core = await loadPagesCore();
5050
+ const isScopeParent = Number(parent.isScope) === 1;
5051
+ const newScopeId = isScopeParent ? String(parent.id) : String(parent.scopeId);
5052
+ const newDepth = isScopeParent ? 1 : Number(parent.depth) + 1;
5053
+ const oldPath = String(root.path);
5054
+ const within = prefixRange("path", oldPath);
5055
+ const live = (await this.#query(tx, `SELECT id, ${KEY}, depth, deletedAt FROM ${T.pages} WHERE ${within.sql}`, within.args)).filter((row) => !row.deletedAt);
5056
+ if (live.length > core.MAX_KNOWLEDGE_MOVE_PAGES) throw new core.KnowledgeMoveTooLargeError(options.countLimitId, live.length);
5057
+ const shift = newDepth - Number(root.depth);
5058
+ if (Math.max(...live.map((row) => Number(row.depth))) + shift > core.MAX_KNOWLEDGE_PAGE_DEPTH) throw new core.KnowledgePageDepthError(String(root.id));
5059
+ if (!root.deletedAt && root.key !== null) {
5060
+ if ((await this.#query(tx, `SELECT id FROM ${T.pages} WHERE parentId = ? AND isScope = 0 AND ${KEY} = ? AND deletedAt IS NULL AND id <> ?`, [
5061
+ String(parent.id),
5062
+ String(root.key),
5063
+ String(root.id)
5064
+ ])).length > 0) throw new core.KnowledgePageKeyConflictError(String(root.key), String(parent.id));
5065
+ }
5066
+ const newPath = `${String(parent.path)}${String(root.id)}/`;
5067
+ const position = options.position ?? core.knowledgePositionBetween(await this.#lastPosition(tx, String(parent.id), false));
5068
+ await tx.execute({
5069
+ sql: `UPDATE ${T.pages} SET path = CONCAT(?, SUBSTRING(path, CHAR_LENGTH(?) + 1)), depth = depth + ?, scopeId = ? WHERE ${within.sql}`,
5070
+ args: [
5071
+ newPath,
5072
+ oldPath,
5073
+ shift,
5074
+ newScopeId,
5075
+ ...within.args
5076
+ ]
5077
+ });
5078
+ await tx.execute({
5079
+ sql: `UPDATE ${T.pages} SET parentId = ?, position = ? WHERE id = ?`,
5080
+ args: [
5081
+ String(parent.id),
5082
+ position,
5083
+ String(root.id)
5084
+ ]
5085
+ });
5086
+ return {
5087
+ moved: live.length,
5088
+ scopeId: newScopeId
5089
+ };
5090
+ }
5091
+ movePage(input) {
4559
5092
  return this.#transaction(async (tx) => {
4560
- const source = await this.#getNode(tx, input.sourceId);
4561
- if (!source) throw new _mastra_core_storage.KnowledgeNotFoundError("node", input.sourceId);
4562
- const target = await this.#resolveTerminalNode(tx, input.targetId);
4563
- if (!target) throw new _mastra_core_storage.KnowledgeNotFoundError("node", input.targetId);
4564
- if (target.id === source.id) throw new Error("Cannot create a knowledge merge cycle");
4565
- if (!(0, _mastra_core_storage.isKnowledgeScopeVisible)(target.scope, source.scope)) throw new Error("Cannot merge a knowledge node into a target that is narrower than its source scope");
4566
- const affected = await tx.execute({
4567
- sql: `SELECT DISTINCT m.sourceType,m.sourceId,COALESCE(f.scope,r.scope) AS scopeJson,CASE WHEN f.deletedAt IS NULL THEN 0 ELSE 1 END AS deleted FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_MENTIONS}" m LEFT JOIN "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" f ON m.sourceType='record' AND f.id=m.sourceId LEFT JOIN "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" r ON m.sourceType='node' AND r.id=m.sourceId WHERE m.recordId=?`,
4568
- args: [source.id]
4569
- });
4570
- const movedFacts = await tx.execute({
4571
- sql: `SELECT id,scope AS scopeJson,deletedAt FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" WHERE node=?`,
4572
- args: [source.id]
4573
- });
4574
- if ((await tx.execute({
4575
- sql: `UPDATE "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" SET mergedInto=?,version=version+1,updatedAt=? WHERE id=? AND type='node' AND version=? AND mergedInto IS NULL`,
5093
+ const row = await this.#livePage(tx, input.id);
5094
+ if (Number(row.isScope) === 1) throw new Error(`Cannot move ${input.id}: it is a scope page`);
5095
+ if (Number(row.version) !== input.expectedVersion) throw new _mastra_core_storage.KnowledgeConflictError(input.id);
5096
+ const parent = await this.#livePage(tx, input.parentId);
5097
+ const path = String(row.path);
5098
+ if (String(parent.id) === input.id || String(parent.path).startsWith(path)) throw new Error(`Cannot move ${input.id} into its own subtree`);
5099
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5100
+ const { scopeId } = await this.#rehome(tx, row, parent, {
5101
+ position: input.position,
5102
+ countLimitId: input.id
5103
+ });
5104
+ await tx.execute({
5105
+ sql: `UPDATE ${T.pages} SET version = version + 1, updatedBy = ?, updatedAt = ? WHERE id = ?`,
4576
5106
  args: [
4577
- target.id,
4578
- (/* @__PURE__ */ new Date()).toISOString(),
4579
- source.id,
4580
- input.sourceVersion
5107
+ JSON.stringify(input.provenance),
5108
+ now,
5109
+ input.id
4581
5110
  ]
4582
- })).rowsAffected === 0) throw new _mastra_core_storage.KnowledgeConflictError(source.id);
5111
+ });
5112
+ await this.#logActivity(tx, "page-moved", {
5113
+ scopeId,
5114
+ pageId: input.id,
5115
+ provenance: input.provenance
5116
+ }, now);
5117
+ return toPage(await this.#pageRow(tx, input.id));
5118
+ });
5119
+ }
5120
+ async mergePages(input) {
5121
+ const core = await loadPagesCore();
5122
+ return this.#transaction(async (tx) => {
5123
+ const source = await this.#livePage(tx, input.sourceId);
5124
+ const target = await this.#livePage(tx, input.targetId);
5125
+ if (Number(source.isScope) === 1 || Number(target.isScope) === 1) throw new Error("Cannot merge a scope page: merging applies to non-scope pages only");
5126
+ if (Number(source.version) !== input.sourceVersion) throw new _mastra_core_storage.KnowledgeConflictError(input.sourceId);
5127
+ const sourcePath = String(source.path);
5128
+ if (input.sourceId === input.targetId || String(target.path).startsWith(sourcePath)) throw new Error(`Page ${input.targetId} is inside page ${input.sourceId}; it cannot be the merge target (subtree)`);
5129
+ const within = prefixRange("path", sourcePath);
5130
+ const carried = await this.#query(tx, `SELECT COUNT(*) AS n FROM ${T.pages} WHERE ${within.sql} AND id <> ? AND deletedAt IS NULL`, [...within.args, input.sourceId]);
5131
+ if (Number(carried[0].n) > core.MAX_KNOWLEDGE_MOVE_PAGES) throw new core.KnowledgeMoveTooLargeError(input.sourceId, Number(carried[0].n));
5132
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4583
5133
  await tx.execute({
4584
- sql: `UPDATE "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" SET node=? WHERE node=?`,
4585
- args: [target.id, source.id]
5134
+ sql: `UPDATE ${T.pages} SET deletedAt = ?, deletedBy = ?, mergedInto = ?, scopeId = ?, version = version + 1, updatedBy = ?, updatedAt = ? WHERE id = ?`,
5135
+ args: [
5136
+ now,
5137
+ input.provenance.author,
5138
+ input.targetId,
5139
+ String(target.scopeId),
5140
+ JSON.stringify(input.provenance),
5141
+ now,
5142
+ input.sourceId
5143
+ ]
5144
+ });
5145
+ const children = await this.#query(tx, `SELECT * FROM ${T.pages} WHERE parentId = ? AND isScope = 0`, [input.sourceId]);
5146
+ for (const child of children) await this.#rehome(tx, child, target, {
5147
+ position: String(child.position),
5148
+ countLimitId: input.sourceId
4586
5149
  });
4587
5150
  await tx.execute({
4588
- sql: `DELETE source FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_MENTIONS}" source JOIN "${_mastra_core_storage.TABLE_KNOWLEDGE_MENTIONS}" target ON target.sourceType=source.sourceType AND target.sourceId=source.sourceId WHERE source.recordId=? AND target.recordId=?`,
4589
- args: [source.id, target.id]
5151
+ sql: `UPDATE ${T.notes} SET pageId = ? WHERE pageId = ?`,
5152
+ args: [input.targetId, input.sourceId]
4590
5153
  });
4591
5154
  await tx.execute({
4592
- sql: `UPDATE "${_mastra_core_storage.TABLE_KNOWLEDGE_MENTIONS}" SET recordId=? WHERE recordId=?`,
4593
- args: [target.id, source.id]
4594
- });
4595
- for (const row of movedFacts.rows) await this.#outbox(tx, "record", String(row.id), row.deletedAt == null ? "upsert" : "delete", (0, _mastra_core_storage.createKnowledgeUlid)(), parseJson$1(row.scopeJson));
4596
- for (const row of affected.rows) await this.#outbox(tx, String(row.sourceType), String(row.sourceId), Number(row.deleted) ? "delete" : "upsert", (0, _mastra_core_storage.createKnowledgeUlid)(), parseJson$1(row.scopeJson));
4597
- let mergedTarget = target;
4598
- if (target.description === void 0 && source.description) {
4599
- const adoptedAt = /* @__PURE__ */ new Date();
4600
- if ((await tx.execute({
4601
- sql: `UPDATE "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" SET description=?,version=version+1,updatedAt=? WHERE id=? AND type='node' AND version=? AND description IS NULL`,
5155
+ sql: `UPDATE IGNORE ${T.mentions} SET pageId = ? WHERE pageId = ?`,
5156
+ args: [input.targetId, input.sourceId]
5157
+ });
5158
+ await tx.execute({
5159
+ sql: `DELETE FROM ${T.mentions} WHERE pageId = ?`,
5160
+ args: [input.sourceId]
5161
+ });
5162
+ const pins = await this.#query(tx, `SELECT * FROM ${T.pins} WHERE pageId IN (?, ?)`, [input.sourceId, input.targetId]);
5163
+ const sourcePin = pins.find((pin) => String(pin.pageId) === input.sourceId);
5164
+ if (sourcePin) {
5165
+ const targetPin = pins.find((pin) => String(pin.pageId) === input.targetId);
5166
+ const audience = parseJsonList(sourcePin.audience);
5167
+ if (targetPin) {
5168
+ const merged = [.../* @__PURE__ */ new Set([...parseJsonList(targetPin.audience), ...audience])];
5169
+ await tx.execute({
5170
+ sql: `UPDATE ${T.pins} SET audience = ?, updatedAt = ? WHERE id = ?`,
5171
+ args: [
5172
+ JSON.stringify(merged),
5173
+ now,
5174
+ String(targetPin.id)
5175
+ ]
5176
+ });
5177
+ await this.#indexAudience(tx, String(targetPin.id), merged);
5178
+ await this.#indexAudience(tx, String(sourcePin.id), []);
5179
+ await tx.execute({
5180
+ sql: `DELETE FROM ${T.pins} WHERE id = ?`,
5181
+ args: [String(sourcePin.id)]
5182
+ });
5183
+ } else await tx.execute({
5184
+ sql: `UPDATE ${T.pins} SET pageId = ?, updatedAt = ? WHERE id = ?`,
4602
5185
  args: [
4603
- source.description,
4604
- adoptedAt.toISOString(),
4605
- target.id,
4606
- target.version
5186
+ input.targetId,
5187
+ now,
5188
+ String(sourcePin.id)
4607
5189
  ]
4608
- })).rowsAffected > 0) {
4609
- mergedTarget = {
4610
- ...target,
4611
- description: source.description,
4612
- version: target.version + 1,
4613
- updatedAt: adoptedAt
4614
- };
4615
- await this.#activity(tx, "node-updated", "node", target.id, target.scope);
4616
- }
5190
+ });
4617
5191
  }
4618
- await this.#activity(tx, "node-merged", "node", source.id, source.scope);
4619
- await this.#outbox(tx, "node", source.id, "delete", input.sourceVersion + 1, source.scope);
4620
- await this.#outbox(tx, "node", target.id, "upsert", (0, _mastra_core_storage.createKnowledgeUlid)(), mergedTarget.scope);
4621
- return mergedTarget;
5192
+ await this.#logActivity(tx, "page-merged", {
5193
+ scopeId: String(target.scopeId),
5194
+ pageId: input.sourceId,
5195
+ provenance: input.provenance
5196
+ }, now);
5197
+ return toPage(await this.#pageRow(tx, input.targetId));
4622
5198
  });
4623
5199
  }
4624
- async appendKnowledge(input) {
4625
- await assertKnowledgeRecordTextWithinBoundCompat(input.text);
4626
- const scope = (0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.scope);
4627
- const resolutionScope = (0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.resolutionScope);
4628
- const defaultScope = (0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.defaultScope);
5200
+ async deletePage(input) {
5201
+ const core = await loadPagesCore();
4629
5202
  return this.#transaction(async (tx) => {
4630
- const parent = await this.#resolveTerminalNode(tx, nodeReferenceId(input.node));
4631
- if (!parent) throw new _mastra_core_storage.KnowledgeNotFoundError("node", nodeReferenceId(input.node));
4632
- const record = {
4633
- id: input.id ?? (0, _mastra_core_storage.createKnowledgeUlid)(),
4634
- node: parent.id,
4635
- text: input.text,
4636
- scope,
4637
- sourceThreadId: input.sourceThreadId,
4638
- capturedAt: /* @__PURE__ */ new Date(),
4639
- when: input.when ? new Date(input.when) : void 0,
4640
- metadata: input.metadata
4641
- };
5203
+ const row = await this.#livePage(tx, input.id);
5204
+ if (Number(row.isScope) === 1) throw new Error(`Cannot delete ${input.id}: it is a scope page`);
5205
+ if (Number(row.version) !== input.expectedVersion) throw new _mastra_core_storage.KnowledgeConflictError(input.id);
5206
+ if ((await this.#query(tx, `SELECT 1 FROM ${T.pages} WHERE parentId = ? AND deletedAt IS NULL LIMIT 1`, [input.id])).length > 0) throw new core.KnowledgePageHasChildrenError(input.id);
5207
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4642
5208
  await tx.execute({
4643
- sql: `INSERT INTO "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" (id,node,text,scope,scopeKey,sourceThreadId,capturedAt,"when",metadata,deletedAt,deletedBy) VALUES (?,?,?,jsonb(?),?,?,?,?,jsonb(?),NULL,NULL)`,
5209
+ sql: `UPDATE ${T.pages} SET deletedAt = ?, deletedBy = ?, version = version + 1, updatedBy = ?, updatedAt = ? WHERE id = ?`,
4644
5210
  args: [
4645
- record.id,
4646
- record.node,
4647
- record.text,
4648
- JSON.stringify(scope),
4649
- (0, _mastra_core_storage.knowledgeScopeKey)(scope),
4650
- record.sourceThreadId,
4651
- record.capturedAt.toISOString(),
4652
- record.when?.toISOString() ?? null,
4653
- record.metadata ? JSON.stringify(record.metadata) : null
5211
+ now,
5212
+ input.deletedBy,
5213
+ JSON.stringify({ author: input.deletedBy }),
5214
+ now,
5215
+ input.id
4654
5216
  ]
4655
5217
  });
4656
- await this.#replaceMentions(tx, "record", record.id, record.text, resolutionScope, defaultScope);
4657
- await this.#activity(tx, "record-created", "record", record.id, scope, record.sourceThreadId);
4658
- await this.#outbox(tx, "record", record.id, "upsert", record.id, scope);
4659
- return record;
4660
- });
4661
- }
4662
- async getKnowledge(input) {
4663
- const result = await this.#client.execute({
4664
- sql: `SELECT *,scope AS scopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" WHERE id=?${input.includeDeleted ? "" : " AND deletedAt IS NULL"}`,
4665
- args: [input.id]
5218
+ await tx.execute({
5219
+ sql: `DELETE FROM ${T.pinAudiences} WHERE pinId IN (SELECT id FROM ${T.pins} WHERE pageId = ?)`,
5220
+ args: [input.id]
5221
+ });
5222
+ await tx.execute({
5223
+ sql: `DELETE FROM ${T.pins} WHERE pageId = ?`,
5224
+ args: [input.id]
5225
+ });
5226
+ await this.#logActivity(tx, "page-deleted", {
5227
+ scopeId: String(row.scopeId),
5228
+ pageId: input.id,
5229
+ provenance: { author: input.deletedBy }
5230
+ }, now);
5231
+ return toPage(await this.#pageRow(tx, input.id));
4666
5232
  });
4667
- return result.rows[0] ? parseKnowledge(result.rows[0]) : null;
4668
- }
4669
- async listKnowledgeAbout(input) {
4670
- return this.#queryKnowledge(input, "about");
4671
- }
4672
- async listKnowledgeMentioning(input) {
4673
- return this.#queryKnowledge(input, "mentioning");
4674
5233
  }
4675
- async listKnowledgeRelatedTo(input) {
4676
- return this.#queryKnowledge(input, "related");
4677
- }
4678
- async knowledgeBySource(input) {
4679
- const keys = visibleScopeKeys((0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.scope));
4680
- const args = [input.sourceThreadId, ...keys];
4681
- if (input.after) args.push(input.after);
4682
- const limit = input.limit ?? 100;
5234
+ async #hydrateNotes(executor, rows) {
5235
+ if (rows.length === 0) return [];
5236
+ const ids = jsonList(rows.map((row) => String(row.id)));
5237
+ const stamps = await this.#query(executor, `SELECT noteId, scopeId FROM ${T.stamps} WHERE noteId IN ${JV} ORDER BY scopeId`, [ids]);
5238
+ const mentions = await this.#query(executor, `SELECT noteId, pageId FROM ${T.mentions} WHERE noteId IN ${JV} ORDER BY pageId`, [ids]);
5239
+ return rows.map((row) => toNote(row, stamps.filter((stamp) => String(stamp.noteId) === String(row.id)).map((stamp) => String(stamp.scopeId)), mentions.filter((mention) => String(mention.noteId) === String(row.id)).map((mention) => String(mention.pageId))));
5240
+ }
5241
+ async #noteById(executor, id) {
5242
+ const rows = await this.#query(executor, `SELECT * FROM ${T.notes} WHERE id = ?`, [id]);
5243
+ return (await this.#hydrateNotes(executor, rows))[0] ?? null;
5244
+ }
5245
+ async getNote(input) {
5246
+ const readable = noteReadable("n", input.read);
5247
+ const rows = await this.#query(this.#read, `SELECT n.* FROM ${T.notes} n WHERE n.id = ?${input.includeDeleted ? "" : " AND n.deletedAt IS NULL"} AND ${readable.sql}`, [input.id, ...readable.args]);
5248
+ return (await this.#hydrateNotes(this.#read, rows))[0] ?? null;
5249
+ }
5250
+ async listNotes(input) {
5251
+ const core = await loadPagesCore();
5252
+ const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
5253
+ const cursor = decodeCursor(input.cursor, 2);
5254
+ const readable = noteReadable("n", input.read);
5255
+ const args = [input.pageId, ...readable.args];
5256
+ let after = "";
5257
+ if (cursor) {
5258
+ after = ` AND (n.createdAt > ? OR (n.createdAt = ? AND n.id > ?))`;
5259
+ args.push(cursor[0], cursor[0], cursor[1]);
5260
+ }
4683
5261
  args.push(limit + 1);
4684
- const records = (await this.#client.execute({
4685
- sql: `SELECT *,scope AS scopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" WHERE sourceThreadId=? AND ${visibleSql(keys)}${input.includeDeleted ? "" : " AND deletedAt IS NULL"}${input.after ? " AND id > ?" : ""} ORDER BY id ASC LIMIT ?`,
4686
- args
4687
- })).rows.map(parseKnowledge);
5262
+ const rows = await this.#query(this.#read, `SELECT n.* FROM ${T.notes} n WHERE n.pageId = ?${input.includeDeleted ? "" : " AND n.deletedAt IS NULL"} AND ${readable.sql}${after} ORDER BY n.createdAt, n.id LIMIT ?`, args);
5263
+ return this.#noteOutput(rows, limit);
5264
+ }
5265
+ async countNotes(input) {
5266
+ const ids = [...new Set(input.pageIds)];
5267
+ const counts = Object.fromEntries(ids.map((id) => [id, 0]));
5268
+ if (ids.length === 0) return counts;
5269
+ const readable = noteReadable("n", input.read);
5270
+ const rows = await this.#query(this.#read, `SELECT n.pageId AS pageId, COUNT(*) AS total FROM ${T.notes} n WHERE n.pageId IN ${JV} AND n.deletedAt IS NULL AND ${readable.sql} GROUP BY n.pageId`, [jsonList(ids), ...readable.args]);
5271
+ for (const row of rows) counts[String(row.pageId)] = Number(row.total);
5272
+ return counts;
5273
+ }
5274
+ async listNotesMentioning(input) {
5275
+ const core = await loadPagesCore();
5276
+ const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
5277
+ const cursor = decodeCursor(input.cursor, 2);
5278
+ const readable = noteReadable("n", input.read);
5279
+ const args = [
5280
+ input.pageId,
5281
+ input.pageId,
5282
+ ...readable.args
5283
+ ];
5284
+ let after = "";
5285
+ if (cursor) {
5286
+ after = ` AND (n.createdAt < ? OR (n.createdAt = ? AND n.id < ?))`;
5287
+ args.push(cursor[0], cursor[0], cursor[1]);
5288
+ }
5289
+ args.push(limit + 1);
5290
+ const rows = await this.#query(this.#read, `SELECT n.* FROM ${T.notes} n WHERE n.pageId <> ? AND n.deletedAt IS NULL AND EXISTS (SELECT 1 FROM ${T.mentions} m WHERE m.noteId = n.id AND m.pageId = ?) AND ${readable.sql}${after} ORDER BY n.createdAt DESC, n.id DESC LIMIT ?`, args);
5291
+ return this.#noteOutput(rows, limit);
5292
+ }
5293
+ async #noteOutput(rows, limit) {
5294
+ const page = rows.slice(0, limit);
4688
5295
  return {
4689
- records: records.slice(0, limit),
4690
- nextCursor: records.length > limit ? records[limit - 1]?.id : void 0
5296
+ notes: await this.#hydrateNotes(this.#read, page),
5297
+ ...rows.length > limit ? { nextCursor: encodeCursor([String(page[page.length - 1].createdAt), String(page[page.length - 1].id)]) } : {}
4691
5298
  };
4692
5299
  }
4693
- async removeKnowledge(input) {
5300
+ async #assertScopes(tx, ids) {
5301
+ if (ids.length === 0) throw new Error("A note needs at least one stamp scope");
5302
+ const found = await this.#query(tx, `SELECT id FROM ${T.pages} WHERE isScope = 1 AND id IN ${JV}`, [jsonList(ids)]);
5303
+ const known = new Set(found.map((row) => String(row.id)));
5304
+ const missing = ids.find((id) => !known.has(id));
5305
+ if (missing) throw new Error(`Note stamp ${missing} is not a scope page`);
5306
+ }
5307
+ async addNote(input) {
5308
+ await assertKnowledgeNoteTextWithinBound(input.text);
4694
5309
  return this.#transaction(async (tx) => {
4695
- const record = await this.#getKnowledge(tx, input.id, true);
4696
- if (!record) throw new _mastra_core_storage.KnowledgeNotFoundError("record", input.id);
4697
- if (record.deletedAt) return record;
4698
- const deletedAt = /* @__PURE__ */ new Date();
5310
+ const page = await this.#livePage(tx, input.pageId);
5311
+ const stamps = [...new Set(input.stampScopeIds)];
5312
+ await this.#assertScopes(tx, stamps);
5313
+ const mentions = [...new Set(input.mentions)];
5314
+ for (const mention of mentions) await this.#livePage(tx, mention);
5315
+ const id = input.id ?? (0, _mastra_core_storage.createKnowledgeUlid)();
5316
+ if ((await this.#query(tx, `SELECT 1 FROM ${T.notes} WHERE id = ?`, [id])).length > 0) throw new _mastra_core_storage.KnowledgeConflictError(id);
5317
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4699
5318
  await tx.execute({
4700
- sql: `UPDATE "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" SET deletedAt=?,deletedBy=? WHERE id=? AND deletedAt IS NULL`,
5319
+ sql: `INSERT INTO ${T.notes} (id, pageId, text, ${WHEN}, provenance, version, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, 1, ?, ?)`,
4701
5320
  args: [
4702
- deletedAt.toISOString(),
4703
- input.deletedBy,
4704
- input.id
5321
+ id,
5322
+ input.pageId,
5323
+ input.text,
5324
+ input.when ? input.when.toISOString() : null,
5325
+ JSON.stringify(input.provenance),
5326
+ now,
5327
+ now
4705
5328
  ]
4706
5329
  });
4707
- await this.#activity(tx, "record-deleted", "record", input.id, record.scope, record.sourceThreadId);
4708
- await this.#outbox(tx, "record", input.id, "delete", deletedAt.toISOString(), record.scope);
4709
- return {
4710
- ...record,
4711
- deletedAt,
4712
- deletedBy: input.deletedBy
4713
- };
5330
+ for (const stamp of stamps) await tx.execute({
5331
+ sql: `INSERT INTO ${T.stamps} (noteId, scopeId) VALUES (?, ?)`,
5332
+ args: [id, stamp]
5333
+ });
5334
+ for (const mention of mentions) await tx.execute({
5335
+ sql: `INSERT INTO ${T.mentions} (noteId, pageId) VALUES (?, ?)`,
5336
+ args: [id, mention]
5337
+ });
5338
+ await this.#logActivity(tx, "note-added", {
5339
+ scopeId: String(page.scopeId),
5340
+ pageId: input.pageId,
5341
+ noteId: id,
5342
+ provenance: input.provenance
5343
+ }, now);
5344
+ return await this.#noteById(tx, id);
4714
5345
  });
4715
5346
  }
4716
- async restoreKnowledge(input) {
5347
+ async #noteWithPage(tx, id) {
5348
+ const note = (await this.#query(tx, `SELECT * FROM ${T.notes} WHERE id = ?`, [id]))[0];
5349
+ if (!note) throw new _mastra_core_storage.KnowledgeNotFoundError("note", id);
5350
+ const page = await this.#pageRow(tx, String(note.pageId));
5351
+ if (!page) throw new _mastra_core_storage.KnowledgeNotFoundError("page", String(note.pageId));
5352
+ return {
5353
+ note,
5354
+ page
5355
+ };
5356
+ }
5357
+ restampNote(input) {
4717
5358
  return this.#transaction(async (tx) => {
4718
- const record = await this.#getKnowledge(tx, input.id, true);
4719
- if (!record) throw new _mastra_core_storage.KnowledgeNotFoundError("record", input.id);
4720
- if (!record.deletedAt) return record;
5359
+ const { note, page } = await this.#noteWithPage(tx, input.id);
5360
+ if (note.deletedAt) throw new _mastra_core_storage.KnowledgeNotFoundError("note", input.id);
5361
+ if (Number(note.version) !== input.expectedVersion) throw new _mastra_core_storage.KnowledgeConflictError(input.id);
5362
+ const stamps = [...new Set(input.stampScopeIds)];
5363
+ await this.#assertScopes(tx, stamps);
5364
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4721
5365
  await tx.execute({
4722
- sql: `UPDATE "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" SET deletedAt=NULL,deletedBy=NULL WHERE id=?`,
5366
+ sql: `DELETE FROM ${T.stamps} WHERE noteId = ?`,
4723
5367
  args: [input.id]
4724
5368
  });
4725
- await this.#activity(tx, "record-restored", "record", input.id, record.scope, record.sourceThreadId);
4726
- await this.#outbox(tx, "record", input.id, "upsert", (0, _mastra_core_storage.createKnowledgeUlid)(), record.scope);
4727
- return {
4728
- ...record,
4729
- deletedAt: void 0,
4730
- deletedBy: void 0
4731
- };
5369
+ for (const stamp of stamps) await tx.execute({
5370
+ sql: `INSERT INTO ${T.stamps} (noteId, scopeId) VALUES (?, ?)`,
5371
+ args: [input.id, stamp]
5372
+ });
5373
+ await tx.execute({
5374
+ sql: `UPDATE ${T.notes} SET version = version + 1, updatedAt = ? WHERE id = ?`,
5375
+ args: [now, input.id]
5376
+ });
5377
+ await this.#logActivity(tx, "note-restamped", {
5378
+ scopeId: String(page.scopeId),
5379
+ pageId: String(page.id),
5380
+ noteId: input.id,
5381
+ provenance: input.provenance
5382
+ }, now);
5383
+ return await this.#noteById(tx, input.id);
4732
5384
  });
4733
5385
  }
4734
- async rescopeKnowledge(input) {
4735
- const scope = (0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.scope);
5386
+ removeNote(input) {
4736
5387
  return this.#transaction(async (tx) => {
4737
- const record = await this.#getKnowledge(tx, input.id, true);
4738
- if (!record) throw new _mastra_core_storage.KnowledgeNotFoundError("record", input.id);
5388
+ const { note, page } = await this.#noteWithPage(tx, input.id);
5389
+ if (note.deletedAt) throw new _mastra_core_storage.KnowledgeNotFoundError("note", input.id);
5390
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4739
5391
  await tx.execute({
4740
- sql: `UPDATE "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" SET scope=jsonb(?),scopeKey=? WHERE id=?`,
5392
+ sql: `UPDATE ${T.notes} SET deletedAt = ?, deletedBy = ?, version = version + 1, updatedAt = ? WHERE id = ?`,
4741
5393
  args: [
4742
- JSON.stringify(scope),
4743
- (0, _mastra_core_storage.knowledgeScopeKey)(scope),
5394
+ now,
5395
+ input.deletedBy,
5396
+ now,
4744
5397
  input.id
4745
5398
  ]
4746
5399
  });
4747
- await this.#activity(tx, "record-rescoped", "record", input.id, scope, record.sourceThreadId);
4748
- if ((0, _mastra_core_storage.knowledgeScopeKey)(record.scope) !== (0, _mastra_core_storage.knowledgeScopeKey)(scope)) await this.#outbox(tx, "record", input.id, "delete", (0, _mastra_core_storage.createKnowledgeUlid)(), record.scope);
4749
- if (!record.deletedAt) await this.#outbox(tx, "record", input.id, "upsert", (0, _mastra_core_storage.createKnowledgeUlid)(), scope);
4750
- return {
4751
- ...record,
4752
- scope
4753
- };
5400
+ await this.#logActivity(tx, "note-removed", {
5401
+ scopeId: String(page.scopeId),
5402
+ pageId: String(page.id),
5403
+ noteId: input.id,
5404
+ provenance: input.provenance
5405
+ }, now);
5406
+ return await this.#noteById(tx, input.id);
4754
5407
  });
4755
5408
  }
4756
- async search(input) {
4757
- const scope = (0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.scope);
4758
- const keys = visibleScopeKeys(scope);
4759
- const normalizedQuery = input.query.trim().toLocaleLowerCase();
4760
- if (!normalizedQuery) return [];
4761
- const query = `%${escapeLikePattern(normalizedQuery)}%`;
4762
- const results = (await this.#client.execute({
4763
- sql: `SELECT *,scope AS scopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" WHERE mergedInto IS NULL AND ${visibleSql(keys)} AND (canonicalName LIKE ? ESCAPE '=' OR lower(COALESCE(kind,'')) LIKE ? ESCAPE '=' OR lower(COALESCE(content,'')) LIKE ? ESCAPE '=' OR lower(COALESCE(description,'')) LIKE ? ESCAPE '=') ORDER BY updatedAt DESC LIMIT ?`,
4764
- args: [
4765
- ...keys,
4766
- query,
4767
- query,
4768
- query,
4769
- query,
4770
- input.limit ?? 20
4771
- ]
4772
- })).rows.map((row) => ({
4773
- type: String(row.type),
4774
- id: String(row.id),
4775
- recordId: String(row.id),
4776
- name: String(row.name),
4777
- text: [
4778
- String(row.name),
4779
- ...row.description ? [String(row.description)] : [],
4780
- ...row.content ? [String(row.content)] : []
4781
- ].join("\n"),
4782
- scope: parseJson$1(row.scopeJson)
4783
- }));
4784
- if (results.length < (input.limit ?? 20)) {
4785
- const records = await this.#client.execute({
4786
- sql: `SELECT f.*,f.scope AS scopeJson,r.name,r.scope AS parentScopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" f JOIN "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" r ON r.id=f.node AND r.type='node' AND r.mergedInto IS NULL WHERE f.deletedAt IS NULL AND ${visibleSql(keys, "f.scopeKey")} AND lower(f.text) LIKE ? ESCAPE '=' ORDER BY f.id DESC LIMIT ?`,
4787
- args: [
4788
- ...keys,
4789
- query,
4790
- (input.limit ?? 20) - results.length
4791
- ]
4792
- });
4793
- results.push(...records.rows.map((row) => {
4794
- const parentVisible = (0, _mastra_core_storage.isKnowledgeScopeVisible)(parseJson$1(row.parentScopeJson), scope);
4795
- return {
4796
- type: "record",
4797
- id: String(row.id),
4798
- recordId: parentVisible ? String(row.node) : String(row.id),
4799
- name: parentVisible ? String(row.name) : "(private node)",
4800
- text: String(row.text),
4801
- scope: parseJson$1(row.scopeJson)
4802
- };
4803
- }));
4804
- }
4805
- return results;
4806
- }
4807
- /**
4808
- * @deprecated Curation cursors were removed. Observation-time curate is the only Knowledge writer and needs no
4809
- * cursor. Always throws.
4810
- */
4811
- async getCurationCursor(_input) {
4812
- throw new Error(KNOWLEDGE_CURATION_CURSOR_REMOVED_MESSAGE);
4813
- }
4814
- /**
4815
- * @deprecated Curation cursors were removed. Observation-time curate is the only Knowledge writer and needs no
4816
- * cursor. Always throws.
4817
- */
4818
- async advanceCurationCursor(_input) {
4819
- throw new Error(KNOWLEDGE_CURATION_CURSOR_REMOVED_MESSAGE);
5409
+ restoreNote(input) {
5410
+ return this.#transaction(async (tx) => {
5411
+ const { note, page } = await this.#noteWithPage(tx, input.id);
5412
+ if (!note.deletedAt) throw new _mastra_core_storage.KnowledgeNotFoundError("note", input.id);
5413
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5414
+ await tx.execute({
5415
+ sql: `UPDATE ${T.notes} SET deletedAt = NULL, deletedBy = NULL, version = version + 1, updatedAt = ? WHERE id = ?`,
5416
+ args: [now, input.id]
5417
+ });
5418
+ await this.#logActivity(tx, "note-restored", {
5419
+ scopeId: String(page.scopeId),
5420
+ pageId: String(page.id),
5421
+ noteId: input.id,
5422
+ provenance: input.provenance
5423
+ }, now);
5424
+ return await this.#noteById(tx, input.id);
5425
+ });
4820
5426
  }
4821
- async listActivity(input) {
4822
- const keys = visibleScopeKeys((0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.scope));
4823
- return (await this.#client.execute({
4824
- sql: `SELECT *,scope AS scopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_ACTIVITY}" WHERE ${visibleSql(keys)}${input.after ? " AND id < ?" : ""} ORDER BY id DESC LIMIT ?`,
4825
- args: [
4826
- ...keys,
4827
- ...input.after ? [input.after] : [],
4828
- input.limit ?? 100
4829
- ]
4830
- })).rows.map((row) => ({
4831
- id: String(row.id),
4832
- action: String(row.action),
4833
- recordType: String(row.recordType),
4834
- recordId: String(row.recordId),
4835
- scope: parseJson$1(row.scopeJson),
4836
- sourceThreadId: row.sourceThreadId == null ? void 0 : String(row.sourceThreadId),
4837
- createdAt: toDate(row.createdAt)
5427
+ async searchPages(input) {
5428
+ const words = knowledgeSearchWords(input.query);
5429
+ if (!words.length) return [];
5430
+ const phrase = input.query.trim().toLowerCase().replace(/\s+/g, " ");
5431
+ const limit = Math.max(1, input.limit ?? 20);
5432
+ const likes = words.map((word) => `%${escapeLike(word)}%`);
5433
+ const readable = pagesInReadableScopes("p", input.read);
5434
+ const scopeFilter = input.scopeIds ? ` AND p.scopeId IN ${JV}` : "";
5435
+ const scopeArgs = input.scopeIds ? [jsonList(input.scopeIds)] : [];
5436
+ const pageMatch = words.map(() => `(LOWER(p.name) LIKE ? ESCAPE '=' OR LOWER(COALESCE(p.description, '')) LIKE ? ESCAPE '=' OR LOWER(COALESCE(p.body, '')) LIKE ? ESCAPE '=')`).join(" AND ");
5437
+ const nameMatch = words.map(() => `LOWER(p.name) LIKE ? ESCAPE '='`).join(" AND ");
5438
+ const hits = (await this.#query(this.#read, `SELECT p.* ${readable.sql} WHERE p.deletedAt IS NULL AND p.isScope = 0${scopeFilter} AND ${pageMatch} ORDER BY CASE WHEN LOCATE(?, p.canonicalName) > 0 THEN 0 WHEN ${nameMatch} THEN 1 ELSE 2 END, p.updatedAt DESC, p.id ASC LIMIT ?`, [
5439
+ ...readable.args,
5440
+ ...scopeArgs,
5441
+ ...likes.flatMap((like) => [
5442
+ like,
5443
+ like,
5444
+ like
5445
+ ]),
5446
+ phrase,
5447
+ ...likes,
5448
+ limit
5449
+ ])).map((row) => ({
5450
+ type: "page",
5451
+ page: toPage(row)
4838
5452
  }));
5453
+ if (hits.length >= limit) return hits;
5454
+ const noteVisible = noteReadable("n", input.read);
5455
+ const noteMatch = words.map(() => `LOWER(n.text) LIKE ? ESCAPE '='`).join(" AND ");
5456
+ const noteRows = await this.#query(this.#read, `SELECT n.* FROM ${T.notes} n JOIN ${T.pages} p ON p.id = n.pageId WHERE n.deletedAt IS NULL AND p.deletedAt IS NULL${scopeFilter} AND ${noteMatch} AND ${noteVisible.sql} ORDER BY n.updatedAt DESC, n.id DESC LIMIT ?`, [
5457
+ ...scopeArgs,
5458
+ ...likes,
5459
+ ...noteVisible.args,
5460
+ limit - hits.length
5461
+ ]);
5462
+ const notes = await this.#hydrateNotes(this.#read, noteRows);
5463
+ for (const note of notes) {
5464
+ const page = await this.#pageRow(this.#read, note.pageId);
5465
+ if (page) hits.push({
5466
+ type: "note",
5467
+ note,
5468
+ page: toPage(page)
5469
+ });
5470
+ }
5471
+ return hits;
5472
+ }
5473
+ async listPageActivity(input) {
5474
+ const core = await loadPagesCore();
5475
+ const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
5476
+ const cursor = decodeCursor(input.cursor, 1);
5477
+ const readable = pageReadable("p", input.read);
5478
+ const noteVisible = noteReadable("n", input.read);
5479
+ const args = [...readable.args];
5480
+ let filters = "";
5481
+ if (input.scopeId !== void 0) {
5482
+ filters += ` AND p.scopeId = ?`;
5483
+ args.push(input.scopeId);
5484
+ }
5485
+ if (input.pageId !== void 0) {
5486
+ filters += ` AND e.pageId = ?`;
5487
+ args.push(input.pageId);
5488
+ }
5489
+ if (cursor) {
5490
+ filters += ` AND e.id < ?`;
5491
+ args.push(cursor[0]);
5492
+ }
5493
+ args.push(...noteVisible.args, limit + 1);
5494
+ const rows = await this.#query(this.#read, `SELECT e.* FROM ${T.activity} e JOIN ${T.pages} p ON p.id = e.pageId WHERE ${readable.sql}${filters} AND (e.noteId IS NULL OR EXISTS (SELECT 1 FROM ${T.notes} n WHERE n.id = e.noteId AND ${noteVisible.sql})) ORDER BY e.id DESC LIMIT ?`, args);
5495
+ const page = rows.slice(0, limit);
5496
+ return {
5497
+ events: page.map((row) => ({
5498
+ id: String(row.id),
5499
+ action: String(row.action),
5500
+ scopeId: String(row.scopeId),
5501
+ pageId: String(row.pageId),
5502
+ ...row.noteId ? { noteId: String(row.noteId) } : {},
5503
+ provenance: provenance(row.provenance),
5504
+ createdAt: new Date(String(row.createdAt))
5505
+ })),
5506
+ ...rows.length > limit ? { nextCursor: encodeCursor([String(page[page.length - 1].id)]) } : {}
5507
+ };
4839
5508
  }
4840
- async listSemanticOutbox(input = {}) {
4841
- const clauses = [];
4842
- const args = [];
4843
- if (input.status) {
4844
- clauses.push("status=?");
4845
- args.push(input.status);
4846
- }
4847
- if (input.scope) {
4848
- const keys = visibleScopeKeys(input.scope);
4849
- clauses.push(visibleSql(keys));
4850
- args.push(...keys);
4851
- }
4852
- args.push(input.limit ?? 100);
4853
- return (await this.#client.execute({
4854
- sql: `SELECT *,scope AS scopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}"${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY createdAt ASC,id ASC LIMIT ?`,
4855
- args
4856
- })).rows.map(parseOutbox);
5509
+ /** Rewrites the audience index rows of one pin to match `audience`. */
5510
+ async #indexAudience(tx, pinId, audience) {
5511
+ await tx.execute({
5512
+ sql: `DELETE FROM ${T.pinAudiences} WHERE pinId = ?`,
5513
+ args: [pinId]
5514
+ });
5515
+ for (const entry of audience) await tx.execute({
5516
+ sql: `INSERT IGNORE INTO ${T.pinAudiences} (audience, pinId) VALUES (?, ?)`,
5517
+ args: [entry, pinId]
5518
+ });
4857
5519
  }
4858
- async claimSemanticOutbox(input) {
4859
- const now = input.now ?? /* @__PURE__ */ new Date();
4860
- const stale = new Date(now.getTime() - (input.claimTimeoutMs ?? 6e4));
5520
+ addPinAudience(input) {
4861
5521
  return this.#transaction(async (tx) => {
4862
- const clauses = [
4863
- `availableAt <= ?`,
4864
- `(status='pending' OR (status='processing' AND claimedAt <= ?))`,
4865
- `NOT EXISTS (SELECT 1 FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}" AS earlier WHERE earlier.documentId = "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}".documentId AND earlier.status != 'completed' AND (earlier.createdAt < "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}".createdAt OR (earlier.createdAt = "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}".createdAt AND earlier.id < "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}".id)))`
4866
- ];
4867
- const args = [now.toISOString(), stale.toISOString()];
4868
- if (input.scope) {
4869
- const keys = visibleScopeKeys(input.scope);
4870
- clauses.push(visibleSql(keys));
4871
- args.push(...keys);
4872
- }
4873
- args.push(input.limit ?? 100);
4874
- const ids = (await tx.execute({
4875
- sql: `SELECT id FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}" WHERE ${clauses.join(" AND ")} ORDER BY createdAt ASC,id ASC LIMIT ? FOR UPDATE SKIP LOCKED`,
4876
- args
4877
- })).rows.map((row) => String(row.id));
4878
- for (const id of ids) await tx.execute({
4879
- sql: `UPDATE "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}" SET status='processing',attempts=attempts+1,claimedAt=?,claimedBy=? WHERE id=?`,
4880
- args: [
4881
- now.toISOString(),
4882
- input.workerId,
4883
- id
4884
- ]
4885
- });
4886
- if (!ids.length) return [];
4887
- return (await tx.execute({
4888
- sql: `SELECT *,scope AS scopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}" WHERE id IN (${ids.map(() => "?").join(",")}) ORDER BY createdAt ASC,id ASC`,
4889
- args: ids
4890
- })).rows.map(parseOutbox);
5522
+ const page = await this.#livePage(tx, input.pageId);
5523
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5524
+ const existing = (await this.#query(tx, `SELECT * FROM ${T.pins} WHERE pageId = ?`, [input.pageId]))[0];
5525
+ if (!existing) {
5526
+ const id = (0, _mastra_core_storage.createKnowledgeUlid)();
5527
+ await tx.execute({
5528
+ sql: `INSERT INTO ${T.pins} (id, pageId, audience, createdBy, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)`,
5529
+ args: [
5530
+ id,
5531
+ input.pageId,
5532
+ JSON.stringify([...new Set(input.audience)]),
5533
+ input.createdBy,
5534
+ now,
5535
+ now
5536
+ ]
5537
+ });
5538
+ await this.#indexAudience(tx, id, [...new Set(input.audience)]);
5539
+ await this.#logActivity(tx, "pin-added", {
5540
+ scopeId: String(page.scopeId),
5541
+ pageId: input.pageId,
5542
+ provenance: { author: input.createdBy }
5543
+ }, now);
5544
+ return toPin((await this.#query(tx, `SELECT * FROM ${T.pins} WHERE id = ?`, [id]))[0]);
5545
+ }
5546
+ const current = parseJsonList(existing.audience);
5547
+ const merged = [.../* @__PURE__ */ new Set([...current, ...input.audience])];
5548
+ if (merged.length !== current.length) {
5549
+ await tx.execute({
5550
+ sql: `UPDATE ${T.pins} SET audience = ?, updatedAt = ? WHERE id = ?`,
5551
+ args: [
5552
+ JSON.stringify(merged),
5553
+ now,
5554
+ String(existing.id)
5555
+ ]
5556
+ });
5557
+ await this.#indexAudience(tx, String(existing.id), merged);
5558
+ await this.#logActivity(tx, "pin-added", {
5559
+ scopeId: String(page.scopeId),
5560
+ pageId: input.pageId,
5561
+ provenance: { author: input.createdBy }
5562
+ }, now);
5563
+ }
5564
+ return toPin((await this.#query(tx, `SELECT * FROM ${T.pins} WHERE id = ?`, [String(existing.id)]))[0]);
4891
5565
  });
4892
5566
  }
4893
- async completeSemanticOutbox(input) {
4894
- if (!input.ids.length) return;
4895
- const now = (/* @__PURE__ */ new Date()).toISOString();
4896
- await this.#transaction(async (tx) => {
4897
- for (const id of input.ids) await tx.execute({
4898
- sql: `UPDATE "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}" SET status='completed',completedAt=? WHERE id=? AND status='processing' AND claimedBy=?`,
5567
+ removePinAudience(input) {
5568
+ return this.#transaction(async (tx) => {
5569
+ const existing = (await this.#query(tx, `SELECT * FROM ${T.pins} WHERE pageId = ?`, [input.pageId]))[0];
5570
+ if (!existing) return null;
5571
+ const current = parseJsonList(existing.audience);
5572
+ const remove = new Set(input.audience);
5573
+ const remaining = current.filter((entry) => !remove.has(entry));
5574
+ if (remaining.length === current.length) return toPin(existing);
5575
+ const page = await this.#pageRow(tx, input.pageId);
5576
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5577
+ if (page) await this.#logActivity(tx, "pin-removed", {
5578
+ scopeId: String(page.scopeId),
5579
+ pageId: input.pageId,
5580
+ provenance: { author: input.removedBy }
5581
+ }, now);
5582
+ if (remaining.length === 0) {
5583
+ await this.#indexAudience(tx, String(existing.id), []);
5584
+ await tx.execute({
5585
+ sql: `DELETE FROM ${T.pins} WHERE id = ?`,
5586
+ args: [String(existing.id)]
5587
+ });
5588
+ return null;
5589
+ }
5590
+ await tx.execute({
5591
+ sql: `UPDATE ${T.pins} SET audience = ?, updatedAt = ? WHERE id = ?`,
4899
5592
  args: [
5593
+ JSON.stringify(remaining),
4900
5594
  now,
4901
- id,
4902
- input.workerId
5595
+ String(existing.id)
4903
5596
  ]
4904
5597
  });
5598
+ await this.#indexAudience(tx, String(existing.id), remaining);
5599
+ return toPin((await this.#query(tx, `SELECT * FROM ${T.pins} WHERE id = ?`, [String(existing.id)]))[0]);
4905
5600
  });
4906
5601
  }
4907
- async releaseSemanticOutbox(input) {
4908
- if (!input.ids.length) return;
4909
- await this.#transaction(async (tx) => {
4910
- for (const id of input.ids) await tx.execute({
4911
- sql: `UPDATE "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}" SET status='pending',availableAt=?,claimedAt=NULL,claimedBy=NULL WHERE id=? AND status='processing' AND claimedBy=?`,
4912
- args: [
4913
- (input.retryAt ?? /* @__PURE__ */ new Date()).toISOString(),
4914
- id,
4915
- input.workerId
4916
- ]
4917
- });
5602
+ async listPins(input) {
5603
+ const core = await loadPagesCore();
5604
+ const limit = limitOf(input.limit, core.MAX_KNOWLEDGE_PAGE_LIST_LIMIT);
5605
+ const cursor = decodeCursor(input.cursor, 2);
5606
+ const readable = pageReadable("p", input.read);
5607
+ const args = [...readable.args];
5608
+ let filters = "";
5609
+ if (input.audience !== "all") {
5610
+ filters += ` AND pin.id IN (SELECT pa.pinId FROM (SELECT jv.v FROM JSON_TABLE(?, '$[*]' COLUMNS (v VARCHAR(512) ${BIN} PATH '$')) jv) aud STRAIGHT_JOIN ${T.pinAudiences} pa ON pa.audience = aud.v)`;
5611
+ args.push(jsonList(input.audience));
5612
+ }
5613
+ if (input.pageIds) {
5614
+ filters += ` AND pin.pageId IN ${JV}`;
5615
+ args.push(jsonList(input.pageIds));
5616
+ }
5617
+ if (input.excludePageIds?.length) {
5618
+ filters += ` AND pin.pageId NOT IN ${JV}`;
5619
+ args.push(jsonList(input.excludePageIds));
5620
+ }
5621
+ if (input.withBody) filters += ` AND REGEXP_LIKE(p.body, '[^[:space:]]')`;
5622
+ const from = `FROM ${T.pins} pin JOIN ${T.pages} p ON p.id = pin.pageId WHERE p.deletedAt IS NULL AND ${readable.sql}${filters}`;
5623
+ let total;
5624
+ if (input.count) total = Number((await this.#query(this.#read, `SELECT COUNT(*) AS n ${from}`, args))[0].n);
5625
+ const pageArgs = [...args];
5626
+ let cursorFilter = "";
5627
+ if (cursor) {
5628
+ cursorFilter = ` AND (pin.createdAt < ? OR (pin.createdAt = ? AND pin.id < ?))`;
5629
+ pageArgs.push(cursor[0], cursor[0], cursor[1]);
5630
+ }
5631
+ pageArgs.push(limit + 1);
5632
+ const rows = await this.#query(this.#read, `SELECT pin.*, p.name AS pageName ${from}${cursorFilter} ORDER BY pin.createdAt DESC, pin.id DESC LIMIT ?`, pageArgs);
5633
+ const page = rows.slice(0, limit);
5634
+ return {
5635
+ pins: page.map(toListedPin),
5636
+ ...rows.length > limit ? { nextCursor: encodeCursor([String(page[page.length - 1].createdAt), String(page[page.length - 1].id)]) } : {},
5637
+ ...total === void 0 ? {} : { total }
5638
+ };
5639
+ }
5640
+ };
5641
+ /** mysql2 returns JSON columns already parsed; accept the raw string form too. */
5642
+ function parseJsonList(value) {
5643
+ return typeof value === "string" ? JSON.parse(value) : value;
5644
+ }
5645
+ // src/storage/domains/knowledge/index.ts
5646
+ var KnowledgeMySQL = class extends _mastra_core_storage.KnowledgeStorage {
5647
+ static getExportDDL() {
5648
+ return KNOWLEDGE_PAGE_DDL.map((statement) => `${statement};`);
5649
+ }
5650
+ #pool;
5651
+ #pages;
5652
+ constructor({ pool }) {
5653
+ super();
5654
+ this.#pool = pool;
5655
+ const rawExecutor = (client) => ({ async execute(statement) {
5656
+ const [result] = await client.query(statement.sql, statement.args ?? []);
5657
+ return { rows: Array.isArray(result) ? result : [] };
5658
+ } });
5659
+ this.#pages = new MySQLKnowledgePages({
5660
+ read: rawExecutor(pool),
5661
+ transaction: async (operation) => {
5662
+ const connection = await pool.getConnection();
5663
+ try {
5664
+ await connection.beginTransaction();
5665
+ const result = await operation(rawExecutor(connection));
5666
+ await connection.commit();
5667
+ return result;
5668
+ } catch (error) {
5669
+ await connection.rollback();
5670
+ throw error;
5671
+ } finally {
5672
+ connection.release();
5673
+ }
5674
+ }
4918
5675
  });
4919
5676
  }
4920
- async #transaction(operation) {
5677
+ getCapabilities() {
5678
+ return {
5679
+ contractVersion: 4,
5680
+ schemaVersion: 4,
5681
+ supportsSchemaInspection: true,
5682
+ supportsExplicitReset: true
5683
+ };
5684
+ }
5685
+ async #inspectPages(client = this.#pool) {
5686
+ return inspectKnowledgePagesSchema(await this.#pagesSnapshot(client));
5687
+ }
5688
+ async #pagesSnapshot(client) {
5689
+ const exec = async (sql) => {
5690
+ const [rows] = await client.query(sql);
5691
+ return rows;
5692
+ };
5693
+ const tableNames = (await exec("SELECT table_name AS name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE 'mastra\\_knowledge\\_%'")).map((row) => String(row.name));
5694
+ let schemaVersion;
5695
+ if (tableNames.includes("mastra_knowledge_access_state")) {
5696
+ const state = await exec(`SELECT schemaVersion FROM \`${TABLE_KNOWLEDGE_PAGES_STATE}\` WHERE id = 'knowledge'`);
5697
+ if (state[0]) schemaVersion = Number(state[0].schemaVersion);
5698
+ }
5699
+ return {
5700
+ tableNames,
5701
+ schemaVersion
5702
+ };
5703
+ }
5704
+ async inspectSchema() {
5705
+ return this.#inspectPages();
5706
+ }
5707
+ /** Runs the earlier-alpha upgrades under a named lock so processes starting together don't race their ALTERs. */
5708
+ async #upgrade() {
4921
5709
  const connection = await this.#pool.getConnection();
4922
5710
  try {
4923
- await connection.beginTransaction();
4924
- const result = await operation(createExecutor(connection));
4925
- await connection.commit();
4926
- return result;
4927
- } catch (error) {
4928
- await connection.rollback();
4929
- throw error;
5711
+ await connection.query("SELECT GET_LOCK('mastra_knowledge_upgrade', 60)");
5712
+ for (const upgrade of KNOWLEDGE_PAGE_UPGRADES) {
5713
+ const [rows] = await connection.query(upgrade.needed, upgrade.args);
5714
+ if (rows.length > 0) for (const statement of upgrade.apply) await connection.query(statement);
5715
+ }
4930
5716
  } finally {
5717
+ await connection.query("SELECT RELEASE_LOCK('mastra_knowledge_upgrade')").catch(() => {});
4931
5718
  connection.release();
4932
5719
  }
4933
5720
  }
4934
- async #getNode(executor, id) {
4935
- const result = await executor.execute({
4936
- sql: `SELECT *,scope AS scopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" WHERE id=? AND type='node'`,
4937
- args: [id]
4938
- });
4939
- return result.rows[0] ? parseNode(result.rows[0]) : null;
4940
- }
4941
- async #getNodeByName(executor, name, scope) {
4942
- const result = await executor.execute({
4943
- sql: `SELECT *,scope AS scopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" WHERE type='node' AND scopeKey=? AND canonicalName=?`,
4944
- args: [(0, _mastra_core_storage.knowledgeScopeKey)(scope), canonicalName(name)]
4945
- });
4946
- return result.rows[0] ? parseNode(result.rows[0]) : null;
4947
- }
4948
- async #resolveNode(executor, name, scope) {
4949
- const keys = visibleScopeKeys(scope);
4950
- const candidates = (await executor.execute({
4951
- sql: `SELECT *,scope AS scopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" WHERE type='node' AND canonicalName=? AND (scopeKey IN (${keys.map(() => "?").join(",")}) OR mergedInto IS NOT NULL)`,
4952
- args: [canonicalName(name), ...keys]
4953
- })).rows.map(parseNode).sort((left, right) => right.scope.length - left.scope.length);
4954
- for (const candidate of candidates) {
4955
- const terminal = await this.#resolveTerminalNode(executor, candidate.id);
4956
- if (terminal && (0, _mastra_core_storage.isKnowledgeScopeVisible)(terminal.scope, scope)) return terminal;
5721
+ async init() {
5722
+ const connection = await this.#pool.getConnection();
5723
+ try {
5724
+ const [locked] = await connection.query("SELECT GET_LOCK('mastra_knowledge_init', 60) AS ok");
5725
+ if (Number(locked[0]?.ok) !== 1) throw new Error("Timed out waiting for the Knowledge schema lock");
5726
+ for (const table of legacyKnowledgeTablesToDrop(await this.#pagesSnapshot(connection))) await connection.query(`DROP TABLE IF EXISTS \`${table}\``);
5727
+ const inspection = await this.#inspectPages(connection);
5728
+ if (inspection.status === "incompatible-reset-required") throw new _mastra_core_storage.KnowledgeSchemaResetRequiredError(inspection);
5729
+ for (const statement of KNOWLEDGE_PAGE_DDL) await connection.query(statement);
5730
+ await connection.query(`INSERT IGNORE INTO \`${TABLE_KNOWLEDGE_PAGES_STATE}\` (id, schemaVersion) VALUES ('knowledge', ?)`, [4]);
5731
+ } finally {
5732
+ await connection.query("SELECT RELEASE_LOCK('mastra_knowledge_init')").catch(() => {});
5733
+ connection.release();
4957
5734
  }
4958
- return null;
5735
+ await this.#upgrade();
4959
5736
  }
4960
- async #resolveTerminalNode(executor, id) {
4961
- let node = await this.#getNode(executor, id);
4962
- const seen = /* @__PURE__ */ new Set();
4963
- while (node?.mergedInto) {
4964
- if (seen.has(node.id)) throw new Error(`Knowledge merge cycle detected at ${node.id}`);
4965
- seen.add(node.id);
4966
- node = await this.#getNode(executor, node.mergedInto);
4967
- }
4968
- return node;
5737
+ async dangerouslyReset() {
5738
+ for (const table of [...KNOWLEDGE_RESETTABLE_TABLE_NAMES, "mastra_knowledge_pin_audiences"]) await this.#pool.query(`DROP TABLE IF EXISTS \`${table}\``);
5739
+ await this.init();
4969
5740
  }
4970
- async #getKnowledge(executor, id, includeDeleted) {
4971
- const result = await executor.execute({
4972
- sql: `SELECT *,scope AS scopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" WHERE id=?${includeDeleted ? "" : " AND deletedAt IS NULL"}`,
4973
- args: [id]
4974
- });
4975
- return result.rows[0] ? parseKnowledge(result.rows[0]) : null;
5741
+ materializeScope(input) {
5742
+ return this.#pages.materializeScope(input);
4976
5743
  }
4977
- async #queryKnowledge(input, relationship) {
4978
- const scope = (0, _mastra_core_storage.canonicalizeKnowledgeScope)(input.scope);
4979
- const node = await this.#resolveTerminalNode(this.#client, nodeReferenceId(input.node));
4980
- if (!node) return { records: [] };
4981
- const keys = visibleScopeKeys(scope);
4982
- const args = [
4983
- node.id,
4984
- ...relationship === "related" ? [node.id] : [],
4985
- ...keys
4986
- ];
4987
- if (input.after) args.push(input.after);
4988
- args.push((input.limit ?? 100) + 1);
4989
- const records = (await this.#client.execute({
4990
- sql: `SELECT DISTINCT f.*,f.scope AS scopeJson FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_RECORDS}" f${relationship === "about" ? "" : ` LEFT JOIN "${_mastra_core_storage.TABLE_KNOWLEDGE_MENTIONS}" m ON m.sourceType='record' AND m.sourceId=f.id`} WHERE ${relationship === "about" ? "f.node=?" : relationship === "mentioning" ? "m.recordId=?" : "(f.node=? OR m.recordId=?)"} AND ${visibleSql(keys, "f.scopeKey")}${input.includeDeleted ? "" : " AND f.deletedAt IS NULL"}${input.after ? " AND f.id < ?" : ""} ORDER BY f.id DESC LIMIT ?`,
4991
- args
4992
- })).rows.map(parseKnowledge);
4993
- const limit = input.limit ?? 100;
4994
- return {
4995
- records: records.slice(0, limit),
4996
- nextCursor: records.length > limit ? records[limit - 1]?.id : void 0
4997
- };
5744
+ listScopeDeclarations() {
5745
+ return this.#pages.listScopeDeclarations();
4998
5746
  }
4999
- async #replaceMentions(tx, sourceType, sourceId, text, resolutionScope, defaultScope) {
5000
- await tx.execute({
5001
- sql: `DELETE FROM "${_mastra_core_storage.TABLE_KNOWLEDGE_MENTIONS}" WHERE sourceType=? AND sourceId=?`,
5002
- args: [sourceType, sourceId]
5003
- });
5004
- for (const name of (0, _mastra_core_storage.parseKnowledgeWikilinks)(text)) {
5005
- let node = await this.#resolveNode(tx, name, resolutionScope);
5006
- if (!node) {
5007
- node = await this.#getNodeByName(tx, name, defaultScope);
5008
- if (node) node = await this.#resolveTerminalNode(tx, node.id);
5009
- if (!node) {
5010
- const now = /* @__PURE__ */ new Date();
5011
- node = {
5012
- id: crypto.randomUUID(),
5013
- type: "node",
5014
- name,
5015
- kind: "node",
5016
- scope: defaultScope,
5017
- version: 1,
5018
- createdAt: now,
5019
- updatedAt: now
5020
- };
5021
- await tx.execute({
5022
- sql: `INSERT INTO "${_mastra_core_storage.TABLE_KNOWLEDGE_NODES}" (id,type,name,canonicalName,kind,content,scope,scopeKey,version,mergedInto,createdAt,updatedAt) VALUES (?,?,?,?,?,NULL,jsonb(?),?,?,NULL,?,?)`,
5023
- args: [
5024
- node.id,
5025
- "node",
5026
- node.name,
5027
- canonicalName(node.name),
5028
- node.kind,
5029
- JSON.stringify(defaultScope),
5030
- (0, _mastra_core_storage.knowledgeScopeKey)(defaultScope),
5031
- 1,
5032
- now.toISOString()
5033
- ]
5034
- });
5035
- await this.#activity(tx, "node-created", "node", node.id, defaultScope);
5036
- await this.#outbox(tx, "node", node.id, "upsert", 1, defaultScope);
5037
- }
5038
- }
5039
- await tx.execute({
5040
- sql: `INSERT OR IGNORE INTO "${_mastra_core_storage.TABLE_KNOWLEDGE_MENTIONS}" (sourceType,sourceId,recordId) VALUES (?,?,?)`,
5041
- args: [
5042
- sourceType,
5043
- sourceId,
5044
- node.id
5045
- ]
5046
- });
5047
- }
5747
+ renameScopeDeclaration(input) {
5748
+ return this.#pages.renameScopeDeclaration(input);
5048
5749
  }
5049
- async #activity(executor, action, recordType, recordId, scope, sourceThreadId) {
5050
- const now = /* @__PURE__ */ new Date();
5051
- await executor.execute({
5052
- sql: `INSERT INTO "${_mastra_core_storage.TABLE_KNOWLEDGE_ACTIVITY}" (id,action,recordType,recordId,scope,scopeKey,sourceThreadId,createdAt) VALUES (?,?,?,?,jsonb(?),?,?,?)`,
5053
- args: [
5054
- (0, _mastra_core_storage.createKnowledgeUlid)(),
5055
- action,
5056
- recordType,
5057
- recordId,
5058
- JSON.stringify(scope),
5059
- (0, _mastra_core_storage.knowledgeScopeKey)(scope),
5060
- sourceThreadId ?? null,
5061
- now.toISOString()
5062
- ]
5063
- });
5750
+ getScopesByAddress(input) {
5751
+ return this.#pages.getScopesByAddress(input);
5064
5752
  }
5065
- async #outbox(executor, documentType, id, operation, version, scope) {
5066
- const documentId = (0, _mastra_core_storage.knowledgeSemanticDocumentId)(documentType, id);
5067
- const idempotencyKey = (0, _mastra_core_storage.knowledgeSemanticIdempotencyKey)(documentId, operation, version);
5068
- const now = /* @__PURE__ */ new Date();
5069
- await executor.execute({
5070
- sql: `INSERT OR IGNORE INTO "${_mastra_core_storage.TABLE_KNOWLEDGE_SEMANTIC_OUTBOX}" (id,idempotencyKey,documentId,documentType,operation,scope,scopeKey,status,attempts,availableAt,claimedAt,claimedBy,createdAt,completedAt) VALUES (?,?,?,?,?,jsonb(?),?,'pending',0,?,NULL,NULL,?,NULL)`,
5071
- args: [
5072
- (0, _mastra_core_storage.createKnowledgeUlid)(),
5073
- idempotencyKey,
5074
- documentId,
5075
- documentType,
5076
- operation,
5077
- JSON.stringify(scope),
5078
- (0, _mastra_core_storage.knowledgeScopeKey)(scope),
5079
- now.toISOString(),
5080
- now.toISOString()
5081
- ]
5082
- });
5753
+ listChildScopes(input) {
5754
+ return this.#pages.listChildScopes(input);
5755
+ }
5756
+ getPage(input) {
5757
+ return this.#pages.getPage(input);
5758
+ }
5759
+ getPageByPath(input) {
5760
+ return this.#pages.getPageByPath(input);
5761
+ }
5762
+ findPagesByName(input) {
5763
+ return this.#pages.findPagesByName(input);
5764
+ }
5765
+ listChildPages(input) {
5766
+ return this.#pages.listChildPages(input);
5767
+ }
5768
+ listPages(input) {
5769
+ return this.#pages.listPages(input);
5770
+ }
5771
+ createPage(input) {
5772
+ return this.#pages.createPage(input);
5773
+ }
5774
+ updatePage(input) {
5775
+ return this.#pages.updatePage(input);
5776
+ }
5777
+ movePage(input) {
5778
+ return this.#pages.movePage(input);
5779
+ }
5780
+ mergePages(input) {
5781
+ return this.#pages.mergePages(input);
5782
+ }
5783
+ deletePage(input) {
5784
+ return this.#pages.deletePage(input);
5785
+ }
5786
+ getNote(input) {
5787
+ return this.#pages.getNote(input);
5788
+ }
5789
+ listNotes(input) {
5790
+ return this.#pages.listNotes(input);
5791
+ }
5792
+ countNotes(input) {
5793
+ return this.#pages.countNotes(input);
5794
+ }
5795
+ listNotesMentioning(input) {
5796
+ return this.#pages.listNotesMentioning(input);
5797
+ }
5798
+ addNote(input) {
5799
+ return this.#pages.addNote(input);
5800
+ }
5801
+ restampNote(input) {
5802
+ return this.#pages.restampNote(input);
5803
+ }
5804
+ removeNote(input) {
5805
+ return this.#pages.removeNote(input);
5806
+ }
5807
+ restoreNote(input) {
5808
+ return this.#pages.restoreNote(input);
5809
+ }
5810
+ searchPages(input) {
5811
+ return this.#pages.searchPages(input);
5812
+ }
5813
+ listPageActivity(input) {
5814
+ return this.#pages.listPageActivity(input);
5815
+ }
5816
+ addPinAudience(input) {
5817
+ return this.#pages.addPinAudience(input);
5818
+ }
5819
+ removePinAudience(input) {
5820
+ return this.#pages.removePinAudience(input);
5821
+ }
5822
+ getPages(input) {
5823
+ return this.#pages.getPages(input);
5824
+ }
5825
+ listPins(input) {
5826
+ return this.#pages.listPins(input);
5827
+ }
5828
+ async dangerouslyClearAll() {
5829
+ await this.#pages.clearAll();
5083
5830
  }
5084
5831
  };
5085
5832
  // src/storage/domains/mcp-clients/index.ts