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