@caelo-cms/provisioning 0.11.5 → 0.11.7

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/agent-db.js CHANGED
@@ -6614,6 +6614,8 @@ var LIST_RESERVED_NAMES = new Set([
6614
6614
  "site_text",
6615
6615
  "recurse"
6616
6616
  ]);
6617
+ var LIST_RESERVED_PREFIXES = ["is_", "has_", "count_"];
6618
+ var LIST_RESERVED_NAMES_CLAUSE = `reserved: ${[...LIST_RESERVED_NAMES].join(", ")}, ${LIST_RESERVED_PREFIXES.map((p) => `${p}*`).join(", ")}`;
6617
6619
  var listNameSchema = string2().regex(/^[a-z][a-z0-9_]{0,63}$/, "must be snake_case (lowercase letters, digits, underscores)");
6618
6620
  var fieldLabel = string2().min(1).max(128);
6619
6621
  var fieldExtSchema = record(string2().regex(/^[a-z0-9-]+$/), unknown());
@@ -6769,6 +6771,22 @@ var listItemInputSchema = lazy(() => object({
6769
6771
  query: record(string2(), unknown()).optional()
6770
6772
  }).strict());
6771
6773
 
6774
+ // ../shared/src/lists/page-type.ts
6775
+ var pageTypeDefSchema = object({
6776
+ slug: string2().regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/),
6777
+ name: string2().min(1).max(200),
6778
+ purpose: string2().min(1).max(1000),
6779
+ collection: boolean2(),
6780
+ templates: array(string2().uuid()).min(1).max(64),
6781
+ defaultTemplate: string2().uuid(),
6782
+ properties: array(fieldDefSchema).max(32).default([]),
6783
+ vocabularies: array(object({
6784
+ list: string2().uuid(),
6785
+ cardinality: _enum(["one", "many"]),
6786
+ required: boolean2().optional()
6787
+ }).strict()).max(16).default([])
6788
+ }).strict();
6789
+
6772
6790
  // ../shared/src/lists/pagination.ts
6773
6791
  var PAGINATION_FOLLOW_UPS = ["index", "noindex"];
6774
6792
  var listPaginationSchema = object({
@@ -6827,7 +6845,7 @@ var noCorruptCss = (value, ctx) => {
6827
6845
  var moduleHtml = string2().max(MODULE_HTML_MAX, `html exceeds ${MODULE_HTML_MAX} bytes`).superRefine(noCorruptMarkup("html"));
6828
6846
  var CDATA_OPEN = "<![CDATA[";
6829
6847
  var noCdata = (value) => !value.includes(CDATA_OPEN);
6830
- var cdataMessage = (field) => `${field} contains an XML CDATA marker (${CDATA_OPEN}) \u2014 module ${field} is stored and served ` + `verbatim, so CDATA wrappers reach the browser unparsed and break rendering. ` + `Remove the wrapper and resend plain ${field === "css" ? "CSS" : "JavaScript"}.`;
6848
+ var cdataMessage = (field) => `${field} contains an XML CDATA marker (${CDATA_OPEN}) \u2014 module ${field} is stored and served ` + `verbatim, so CDATA wrappers reach the browser unparsed and break rendering. ` + `Remove the wrapper: pass plain ${field === "css" ? "CSS" : "JavaScript"}.`;
6831
6849
  var moduleCss = string2().max(MODULE_CSS_MAX, `css exceeds ${MODULE_CSS_MAX} bytes`).refine(noCdata, cdataMessage("css")).superRefine(noCorruptCss);
6832
6850
  var moduleJs = string2().max(MODULE_JS_MAX, `js exceeds ${MODULE_JS_MAX} bytes`).refine(noCdata, cdataMessage("js")).superRefine(noJsonEncodedCode("js"));
6833
6851
  var MODULE_FIELD_PRIMITIVE_KINDS = [
@@ -7026,10 +7044,13 @@ var moduleUpdateSchema = object({
7026
7044
  fields: moduleFieldsArray.optional()
7027
7045
  }).strict();
7028
7046
  var pageTypeSlugSchema = string2().regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/, "page type slug: lowercase letters, digits, hyphens");
7047
+ var pageTypeDistinctFromSchema = array(pageTypeSlugSchema).max(16);
7048
+ var newPageTypeSchema = pageTypeDefSchema.omit({ templates: true, defaultTemplate: true }).extend({ distinctFrom: pageTypeDistinctFromSchema.optional() }).strict();
7029
7049
  var templateCreateSchema = object({
7030
7050
  slug: slugSchema,
7031
7051
  displayName: displayNameSchema,
7032
7052
  pageType: pageTypeSlugSchema.optional(),
7053
+ newPageType: newPageTypeSchema.optional(),
7033
7054
  html: string2().max(TEMPLATE_HTML_MAX, `html exceeds ${TEMPLATE_HTML_MAX} bytes`).superRefine(noCorruptMarkup("html")),
7034
7055
  css: string2().max(TEMPLATE_CSS_MAX, `css exceeds ${TEMPLATE_CSS_MAX} bytes`).default(""),
7035
7056
  layoutId: string2().uuid().optional(),
@@ -7366,7 +7387,8 @@ var chatCreateSessionInput = object({
7366
7387
  templateId: string2().uuid().optional(),
7367
7388
  subagentRole: string2().min(1).max(120).optional(),
7368
7389
  parentChatSessionId: string2().uuid().nullable().optional(),
7369
- isolation: _enum(["experiment", "migration"]).optional()
7390
+ isolation: _enum(["experiment", "migration"]).optional(),
7391
+ agentClient: string2().min(1).max(200).optional()
7370
7392
  }).strict();
7371
7393
  var CHAT_IMAGE_MIMES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
7372
7394
  var CHAT_MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
@@ -7457,6 +7479,7 @@ var createTemplateToolInput = object({
7457
7479
  css: sourceTextArg(string2().max(2000000), "css").default(""),
7458
7480
  layoutId: string2().uuid().optional(),
7459
7481
  pageType: pageTypeSlugSchema.optional(),
7482
+ newPageType: newPageTypeSchema.optional(),
7460
7483
  blocks: array(object({
7461
7484
  name: string2().min(1).max(80),
7462
7485
  displayName: string2().min(1).max(200),
@@ -7484,17 +7507,15 @@ var createLayoutToolInput = object({
7484
7507
  }).strict()).min(1).max(20)
7485
7508
  }).strict();
7486
7509
  var setSiteDefaultsToolInput = object({
7487
- defaultLayoutSlug: slugInputSchema.optional(),
7488
- defaultTemplateSlug: slugInputSchema.optional()
7489
- }).strict().refine((v) => v.defaultLayoutSlug !== undefined || v.defaultTemplateSlug !== undefined, {
7490
- message: "must provide at least one of defaultLayoutSlug, defaultTemplateSlug"
7491
- });
7510
+ defaultLayoutSlug: slugInputSchema
7511
+ }).strict();
7492
7512
  var duplicatePageToolInput = object({
7493
7513
  sourcePageId: string2().uuid(),
7494
7514
  newSlug: slugInputSchema,
7495
7515
  newTitle: string2().min(1).max(256).optional(),
7496
7516
  newShortTitle: pageShortTitleSchema.optional(),
7497
7517
  targetTemplateId: string2().uuid().optional(),
7518
+ pageType: pageTypeSlugSchema.optional(),
7498
7519
  newParentPageId: string2().uuid().nullable().optional()
7499
7520
  }).strict();
7500
7521
  var repointPageTemplateToolInput = object({
@@ -7628,6 +7649,7 @@ var bootstrapSiteScaffoldToolInput = object({
7628
7649
  })).max(8).optional(),
7629
7650
  templateSlug: string2().min(1).max(120).optional(),
7630
7651
  templateDisplayName: string2().min(1).max(256).optional(),
7652
+ templatePageType: newPageTypeSchema.optional(),
7631
7653
  setAsDefaults: boolean2().optional()
7632
7654
  }).strict();
7633
7655
  var revertChatChangesToolInput = object({
@@ -7964,6 +7986,98 @@ var pageModuleContentSetManySchema = object({
7964
7986
  }).strict();
7965
7987
  // ../shared/src/cap-failures.ts
7966
7988
  var counters = new Map;
7989
+ // ../shared/src/change-journal.ts
7990
+ var JOURNAL_KINDS = [
7991
+ "page",
7992
+ "page_terms",
7993
+ "page_seo",
7994
+ "page_layout",
7995
+ "page_module_content",
7996
+ "module",
7997
+ "template",
7998
+ "template_blocks",
7999
+ "layout",
8000
+ "layout_blocks",
8001
+ "layout_module",
8002
+ "content_instance",
8003
+ "list",
8004
+ "list_type",
8005
+ "page_type",
8006
+ "theme",
8007
+ "media",
8008
+ "media_variant",
8009
+ "media_crop",
8010
+ "font",
8011
+ "redirect",
8012
+ "domain",
8013
+ "site_defaults",
8014
+ "site_settings",
8015
+ "site_memory",
8016
+ "skill",
8017
+ "plugin",
8018
+ "plugin_installation",
8019
+ "plugin_grant",
8020
+ "plugin_file",
8021
+ "plugin_row",
8022
+ "plugin_storage",
8023
+ "snapshot"
8024
+ ];
8025
+ var KIND_SET = new Set(JOURNAL_KINDS);
8026
+ var row = (kind, key, pair, extra = {}) => ({ kind, mode: "row", key, pair, ...extra });
8027
+ var snapshot = (kind, key, extra = {}) => ({ kind, mode: "snapshot", key, pair: ["id"], ...extra });
8028
+ var JOURNALED_TABLES = {
8029
+ pages: row("page", "id", ["id"], { branch: "chat_branch_id" }),
8030
+ page_terms: row("page_terms", "page_id", ["page_id", "list_id", "item_id"]),
8031
+ pages_seo: row("page_seo", "page_id", ["page_id"]),
8032
+ page_modules: row("page_layout", "page_id", ["page_id", "block_name", "position"]),
8033
+ page_module_content: row("page_module_content", "id", ["id"], { parent: "page_id" }),
8034
+ modules: row("module", "id", ["id"], { branch: "chat_branch_id" }),
8035
+ templates: row("template", "id", ["id"], { branch: "chat_branch_id" }),
8036
+ template_blocks: row("template_blocks", "template_id", ["template_id", "name"]),
8037
+ layouts: row("layout", "id", ["id"], { branch: "chat_branch_id" }),
8038
+ layout_blocks: row("layout_blocks", "layout_id", ["layout_id", "name"]),
8039
+ layout_modules: row("layout_module", "layout_id", ["layout_id", "block_name", "position"]),
8040
+ content_instances: row("content_instance", "id", ["id"], {
8041
+ branch: "chat_branch_id",
8042
+ parent: "module_id"
8043
+ }),
8044
+ lists: row("list", "id", ["id"], { branch: "chat_branch_id" }),
8045
+ list_types: row("list_type", "id", ["id"], { branch: "chat_branch_id" }),
8046
+ page_types: row("page_type", "id", ["id"], { branch: "chat_branch_id" }),
8047
+ themes: row("theme", "id", ["id"]),
8048
+ media_assets: row("media", "id", ["id"]),
8049
+ media_variants: row("media_variant", "asset_id", ["asset_id", "variant"]),
8050
+ media_crops: row("media_crop", "id", ["id"], { parent: "asset_id" }),
8051
+ font_assets: row("font", "id", ["id"]),
8052
+ redirects: row("redirect", "id", ["id"]),
8053
+ domains: row("domain", "id", ["id"]),
8054
+ site_defaults: row("site_defaults", "id", ["id"]),
8055
+ site_settings: row("site_settings", "id", ["id"]),
8056
+ site_ai_memory: row("site_memory", "id", ["id"]),
8057
+ skills: row("skill", "id", ["id"]),
8058
+ plugins: row("plugin", "id", ["id"]),
8059
+ plugin_installation_versions: row("plugin_installation", "id", ["id"], { parent: "plugin_id" }),
8060
+ plugin_capability_grants: row("plugin_grant", "id", ["id"], { parent: "plugin_id" }),
8061
+ plugin_private_files: row("plugin_file", "id", ["plugin_id", "id"], { plugin: "plugin_id" }),
8062
+ site_snapshots: { kind: "snapshot", mode: "header", key: "id", pair: ["id"] },
8063
+ page_snapshots: snapshot("page", "page_id"),
8064
+ page_layout_snapshots: snapshot("page_layout", "page_id"),
8065
+ page_module_content_snapshots: snapshot("page_module_content", "page_module_content_id", {
8066
+ parent: "page_id"
8067
+ }),
8068
+ module_snapshots: snapshot("module", "module_id"),
8069
+ template_snapshots: snapshot("template", "template_id"),
8070
+ layout_module_snapshots: snapshot("layout_module", "layout_id"),
8071
+ content_instance_snapshots: snapshot("content_instance", "content_instance_id"),
8072
+ list_snapshots: snapshot("list", "list_id"),
8073
+ list_type_snapshots: snapshot("list_type", "list_type_id"),
8074
+ page_type_snapshots: snapshot("page_type", "page_type_id"),
8075
+ theme_snapshots: snapshot("theme", "theme_id"),
8076
+ plugin_row_snapshots: snapshot("plugin_row", "row_id", {
8077
+ parent: "table_name",
8078
+ plugin: "plugin_id"
8079
+ })
8080
+ };
7967
8081
  // ../shared/src/css-scope-scan.ts
7968
8082
  var GROUPING_AT_RULES = new Set(["media", "supports", "layer", "container", "document"]);
7969
8083
  // ../shared/src/interactions.ts
@@ -8026,6 +8140,13 @@ var RASTER_IMAGE_MIMES = new Set([
8026
8140
  ]);
8027
8141
  var mediaUrlPattern = new RegExp(`${MEDIA_URL_PREFIX}/([a-z0-9][a-z0-9-]{0,63})(?:/([a-z][a-z0-9-]{0,63}))?`, "g");
8028
8142
  var sha256Schema = string2().regex(/^[0-9a-f]{64}$/, "must be hex sha256");
8143
+ var publishedDigestSchema = object({
8144
+ contentSha256: sha256Schema,
8145
+ size: number2().int().nonnegative(),
8146
+ gzipSha256: sha256Schema,
8147
+ gzipSize: number2().int().nonnegative(),
8148
+ profile: string2().min(1).max(64)
8149
+ }).strict();
8029
8150
  var mediaVisibilitySchema = _enum(["library", "reference"]);
8030
8151
  var mediaUploadInputSchema = object({
8031
8152
  sha256: sha256Schema,
@@ -8049,7 +8170,8 @@ var mediaUploadInputSchema = object({
8049
8170
  width: number2().int().positive().nullable(),
8050
8171
  height: number2().int().positive().nullable(),
8051
8172
  sizeBytes: number2().int().positive(),
8052
- storageKey: string2().min(1)
8173
+ storageKey: string2().min(1),
8174
+ digest: publishedDigestSchema
8053
8175
  })).min(1)
8054
8176
  }).strict();
8055
8177
  var mediaListInputSchema = object({
@@ -9058,20 +9180,6 @@ var NESTED_KINDS = new Set(["module", "module-list"]);
9058
9180
  // ../shared/src/lists/address.ts
9059
9181
  var SEG = "[a-z0-9_-]+";
9060
9182
  var ADDRESS_RE = new RegExp(`^(?:list/(${SEG})/item/(${SEG})/(${SEG})` + `|instance/(${SEG})/(${SEG})/item/(${SEG})/(${SEG})` + `|page/(${SEG})/props/(${SEG})` + `|list_type/(${SEG})/node/(${SEG})/(${SEG}))$`);
9061
- // ../shared/src/lists/page-type.ts
9062
- var pageTypeDefSchema = object({
9063
- slug: string2().regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/),
9064
- name: string2().min(1).max(200),
9065
- purpose: string2().min(1).max(1000),
9066
- templates: array(string2().uuid()).min(1).max(64),
9067
- defaultTemplate: string2().uuid(),
9068
- properties: array(fieldDefSchema).max(32).default([]),
9069
- vocabularies: array(object({
9070
- list: string2().uuid(),
9071
- cardinality: _enum(["one", "many"]),
9072
- required: boolean2().optional()
9073
- }).strict()).max(16).default([])
9074
- }).strict();
9075
9183
  // ../shared/src/lists/resolve.ts
9076
9184
  var SEG2 = "[a-z0-9_-]+";
9077
9185
  var LIST_VALUE_RE = new RegExp(`^list/(${SEG2})/item/(${SEG2})/(${SEG2})$`);
@@ -9087,7 +9195,8 @@ var LIVE_TOPICS = [
9087
9195
  "credentials",
9088
9196
  "quality",
9089
9197
  "crawl",
9090
- "deploys"
9198
+ "deploys",
9199
+ "draft"
9091
9200
  ];
9092
9201
  var TOPIC_SET = new Set(LIVE_TOPICS);
9093
9202
  // ../shared/src/markup-structure.ts
@@ -9122,8 +9231,43 @@ var pageLogEntrySchema = object({
9122
9231
  // ../shared/src/phase-timer.ts
9123
9232
  var phaseTimingSchema = object({
9124
9233
  phase: string2().min(1),
9125
- ms: number2().int().nonnegative()
9234
+ ms: number2().int().nonnegative(),
9235
+ peakRssMb: number2().int().nonnegative().optional(),
9236
+ cpuMs: number2().int().nonnegative().optional()
9126
9237
  });
9238
+ var RSS_SAMPLE_MS = 25;
9239
+ var MIB = 1024 * 1024;
9240
+ function rssNow() {
9241
+ return typeof process !== "undefined" && typeof process.memoryUsage === "function" ? process.memoryUsage().rss : 0;
9242
+ }
9243
+ var sampler = (() => {
9244
+ const listeners = new Set;
9245
+ let timer = null;
9246
+ return {
9247
+ add(fn) {
9248
+ listeners.add(fn);
9249
+ if (timer === null) {
9250
+ timer = setInterval(() => {
9251
+ const rss = rssNow();
9252
+ for (const l of listeners)
9253
+ l(rss);
9254
+ }, RSS_SAMPLE_MS);
9255
+ timer.unref?.();
9256
+ }
9257
+ },
9258
+ remove(fn) {
9259
+ listeners.delete(fn);
9260
+ if (listeners.size === 0 && timer !== null) {
9261
+ clearInterval(timer);
9262
+ timer = null;
9263
+ }
9264
+ }
9265
+ };
9266
+ })();
9267
+ // ../shared/src/pool-budget.ts
9268
+ var DB_POOL_MAX_ENV = "CAELO_DB_POOL_MAX";
9269
+ var DB_PUBLIC_POOL_MAX_ENV = "CAELO_DB_PUBLIC_POOL_MAX";
9270
+ var MIN_POOL_MAX = 2;
9127
9271
  // ../shared/src/proposal-status.ts
9128
9272
  var PROPOSAL_STATUSES = ["pending", "applied", "rejected", "superseded"];
9129
9273
  var proposalStatus = _enum(PROPOSAL_STATUSES);
@@ -9214,6 +9358,36 @@ var revertPageInput = object({
9214
9358
  pageId: string2().uuid(),
9215
9359
  snapshotId: string2().uuid()
9216
9360
  }).strict();
9361
+ // ../shared/src/stage-status.ts
9362
+ var stagePhaseSchema = _enum([
9363
+ "queued",
9364
+ "preparing",
9365
+ "building",
9366
+ "publishing",
9367
+ "succeeded",
9368
+ "failed"
9369
+ ]);
9370
+ var stageEtaBasisSchema = _enum(["this_run_rate", "last_successful_run"]);
9371
+ var stageEtaSchema = object({
9372
+ remainingMs: number2().int().nonnegative(),
9373
+ basis: stageEtaBasisSchema,
9374
+ includesPublish: boolean2()
9375
+ });
9376
+ var stageStatusRunSchema = object({
9377
+ runId: string2(),
9378
+ targetName: string2(),
9379
+ status: _enum(["queued", "running", "succeeded", "failed"]),
9380
+ phase: stagePhaseSchema,
9381
+ pagesDone: number2().int().nonnegative().nullable(),
9382
+ pagesTotal: number2().int().nonnegative().nullable(),
9383
+ startedAt: string2(),
9384
+ finishedAt: string2().nullable(),
9385
+ elapsedMs: number2().int().nonnegative(),
9386
+ error: string2().nullable(),
9387
+ failedPhase: string2().nullable(),
9388
+ eta: stageEtaSchema.nullable(),
9389
+ etaUnavailable: string2().nullable()
9390
+ });
9217
9391
  // ../shared/src/staging-gate.ts
9218
9392
  var STAGING_SESSION_TTL_SECONDS = 12 * 60 * 60;
9219
9393
  var ENCODER = new TextEncoder;
@@ -18665,7 +18839,8 @@ function extractPgFields(error) {
18665
18839
  let current = error;
18666
18840
  for (let depth = 0;depth < 5 && typeof current === "object" && current !== null; depth++) {
18667
18841
  const e = current;
18668
- const code = typeof e.code === "string" ? e.code : typeof e.errno === "string" ? e.errno : undefined;
18842
+ const sqlstate = typeof e.errno === "string" && /^[0-9A-Z]{5}$/.test(e.errno) ? e.errno : undefined;
18843
+ const code = sqlstate ?? (typeof e.code === "string" ? e.code : undefined);
18669
18844
  const constraint = typeof e.constraint === "string" ? e.constraint : undefined;
18670
18845
  const table = typeof e.table === "string" ? e.table : undefined;
18671
18846
  const column = typeof e.column === "string" ? e.column : undefined;
@@ -18682,6 +18857,11 @@ function extractPgFields(error) {
18682
18857
  out.column = column;
18683
18858
  if (detail)
18684
18859
  out.detail = detail;
18860
+ const message = typeof e.message === "string" ? e.message : "";
18861
+ if (code === "57014" && message.includes("statement timeout"))
18862
+ out.timeout = "statement";
18863
+ if (code === "55P03" && message.includes("lock timeout"))
18864
+ out.timeout = "lock";
18685
18865
  return out;
18686
18866
  }
18687
18867
  current = e.cause;
@@ -18737,6 +18917,88 @@ function defineOperation(def) {
18737
18917
  };
18738
18918
  }
18739
18919
 
18920
+ // ../query-api/src/pg-failure.ts
18921
+ var DATABASE_STATEMENT_TIMEOUT_SECONDS = 60;
18922
+ var DATABASE_LOCK_TIMEOUT_SECONDS = 15;
18923
+ function parseKey(detail) {
18924
+ if (!detail)
18925
+ return null;
18926
+ const start = detail.indexOf("Key (");
18927
+ if (start === -1)
18928
+ return null;
18929
+ const sep = detail.indexOf(")=(", start + 5);
18930
+ const end = detail.lastIndexOf(")");
18931
+ if (sep === -1 || end <= sep + 2)
18932
+ return null;
18933
+ const columns = detail.slice(start + 5, sep);
18934
+ if (columns.includes(")"))
18935
+ return null;
18936
+ return { columns, values: detail.slice(sep + 3, end) };
18937
+ }
18938
+ function keyPhrase(key) {
18939
+ return key.columns.includes(",") ? `the combination \`${key.columns}\` = (${key.values})` : `\`${key.columns}\` "${key.values}"`;
18940
+ }
18941
+ function describePgFailure(fields) {
18942
+ const key = parseKey(fields.detail);
18943
+ switch (fields.code) {
18944
+ case "23505":
18945
+ return {
18946
+ message: key ? `${keyPhrase(key)} already exists \u2014 choose a different value` : "a row with the same unique value already exists \u2014 choose a different value",
18947
+ internal: false
18948
+ };
18949
+ case "23503":
18950
+ if (fields.detail?.includes("still referenced")) {
18951
+ return {
18952
+ message: "the row is still referenced by other content \u2014 remove those references first",
18953
+ internal: false
18954
+ };
18955
+ }
18956
+ return {
18957
+ message: key ? `${keyPhrase(key)} names a row that does not exist or was deleted \u2014 use an id from the matching list tool` : "a referenced row does not exist or was deleted \u2014 use an id from the matching list tool",
18958
+ internal: false
18959
+ };
18960
+ case "23502":
18961
+ return {
18962
+ message: fields.column ? `\`${fields.column}\` is required but was empty` : "a required value was empty",
18963
+ internal: false
18964
+ };
18965
+ case "23514":
18966
+ return { message: "a value is outside the allowed range or set", internal: false };
18967
+ case "22P02":
18968
+ return {
18969
+ message: "a value has the wrong format (e.g. an id that is not a UUID) \u2014 copy ids exactly",
18970
+ internal: false
18971
+ };
18972
+ case "22001":
18973
+ return { message: "a text value is longer than allowed \u2014 shorten it", internal: false };
18974
+ case "22003":
18975
+ return { message: "a number is out of range", internal: false };
18976
+ case "57014":
18977
+ if (fields.timeout === "statement") {
18978
+ return {
18979
+ message: `timed out after ${DATABASE_STATEMENT_TIMEOUT_SECONDS} s (the database's statement timeout) \u2014 a defect in Caelo, reported automatically`,
18980
+ internal: true
18981
+ };
18982
+ }
18983
+ break;
18984
+ case "55P03":
18985
+ if (fields.timeout === "lock") {
18986
+ return {
18987
+ message: `waited ${DATABASE_LOCK_TIMEOUT_SECONDS} s for a row another change holds \u2014 try the call again once that change finished`,
18988
+ internal: false
18989
+ };
18990
+ }
18991
+ break;
18992
+ }
18993
+ return {
18994
+ message: "a database error the call cannot fix \u2014 a defect in Caelo, reported automatically",
18995
+ internal: true
18996
+ };
18997
+ }
18998
+ function rlsDenialMessage(operation, access) {
18999
+ return `permission denied: ${operation} may not ${access} that row as the current actor ` + "(it is not visible on this chat's branch, or belongs to another actor or plugin). " + "Check the ids against the matching list tool; if they are right, the change needs a human with the permission.";
19000
+ }
19001
+
18740
19002
  // ../query-api/src/plugin-schema-provision.ts
18741
19003
  import { createHash } from "crypto";
18742
19004
  var DROPPED_COLUMNS_SETTING = "caelo.plugin_dropped_columns";
@@ -18763,6 +19025,9 @@ async function provisionPluginSchemaInTx(tx, caller, opts) {
18763
19025
  return { schemaName, droppedColumns: [] };
18764
19026
  await tx.execute(sql.raw(opts.sql));
18765
19027
  const dropped = await tx.execute(sql.raw(`SELECT coalesce(current_setting('${DROPPED_COLUMNS_SETTING}', true), '') AS v`));
19028
+ if (opts.journal === true) {
19029
+ await tx.execute(sql`SELECT caelo_journal_attach_plugin_schema(${schemaName}, ${opts.pluginId}::uuid)`);
19030
+ }
18766
19031
  await tx.execute(sql`
18767
19032
  INSERT INTO plugin_schema_provisions (schema_name, sql_digest)
18768
19033
  VALUES (${schemaName}, ${digest})
@@ -18778,18 +19043,25 @@ async function provisionPluginSchemaInTx(tx, caller, opts) {
18778
19043
  // ../query-api/src/pool-guard.ts
18779
19044
  import { AsyncLocalStorage } from "async_hooks";
18780
19045
  var POOL_ACQUIRE_TIMEOUT_MS = 1e4;
18781
- var NESTED_CHECKOUTS = [
18782
- { holder: "plugins.render_hook", issue: 1220 }
18783
- ];
18784
19046
  var LONG_TRANSACTIONS = [
18785
19047
  {
18786
19048
  holder: "static-generator.build",
18787
19049
  reason: "a static site build runs in one transaction for as long as the build takes, idle while plugins render and files are written."
18788
19050
  }
18789
19051
  ];
18790
- var NESTED_CHECKOUT_ALLOWLIST = new Map(NESTED_CHECKOUTS.map((entry) => [entry.holder, entry]));
18791
19052
  var LONG_TRANSACTION_ALLOWLIST = new Map(LONG_TRANSACTIONS.map((entry) => [entry.holder, entry]));
18792
19053
 
19054
+ class SerialQueue {
19055
+ #tail = Promise.resolve();
19056
+ run(fn) {
19057
+ const next = this.#tail.then(fn, fn);
19058
+ this.#tail = next.catch(() => {
19059
+ return;
19060
+ });
19061
+ return next;
19062
+ }
19063
+ }
19064
+
18793
19065
  class NestedPoolCheckoutError extends Error {
18794
19066
  pool;
18795
19067
  holder;
@@ -18815,24 +19087,55 @@ class PoolAcquireTimeoutError extends Error {
18815
19087
  this.timeoutMs = timeoutMs;
18816
19088
  }
18817
19089
  }
18818
- var allowances = new AsyncLocalStorage;
19090
+ var joining = new AsyncLocalStorage;
19091
+ function newMarker(holder, root) {
19092
+ return {
19093
+ holder,
19094
+ active: true,
19095
+ tx: null,
19096
+ identity: null,
19097
+ identityOpName: null,
19098
+ root,
19099
+ joined: new SerialQueue,
19100
+ afterEnd: []
19101
+ };
19102
+ }
19103
+
18819
19104
  class PoolGuard {
18820
19105
  #stores = {
18821
19106
  admin: new AsyncLocalStorage,
18822
19107
  public: new AsyncLocalStorage
18823
19108
  };
18824
- enter(pool, holder) {
18825
- const allowedBy = this.assertMayCheckout(pool, holder);
18826
- return { holder, active: true, allowedBy };
19109
+ plan(pool, holder) {
19110
+ const outer = this.held(pool);
19111
+ if (outer === null)
19112
+ return { kind: "own", marker: newMarker(holder, null) };
19113
+ this.assertMayCheckout(pool, holder);
19114
+ return { kind: "join", outer, marker: newMarker(holder, outer.root ?? outer) };
19115
+ }
19116
+ held(pool) {
19117
+ const outer = this.#stores[pool].getStore();
19118
+ return outer?.active ? outer : null;
18827
19119
  }
18828
19120
  assertMayCheckout(pool, requester) {
18829
- const outer = this.#stores[pool].getStore();
18830
- if (outer === undefined || !outer.active)
18831
- return null;
18832
- const allowedBy = outer.allowedBy ?? (NESTED_CHECKOUT_ALLOWLIST.has(outer.holder) ? outer.holder : null) ?? allowances.getStore() ?? null;
18833
- if (allowedBy === null)
19121
+ const outer = this.held(pool);
19122
+ if (outer === null || joining.getStore() === true)
19123
+ return;
19124
+ throw new NestedPoolCheckoutError(pool, outer.holder, requester);
19125
+ }
19126
+ assertNoHeldTransaction(pool, requester) {
19127
+ const outer = this.held(pool);
19128
+ if (outer !== null)
18834
19129
  throw new NestedPoolCheckoutError(pool, outer.holder, requester);
18835
- return allowedBy;
19130
+ }
19131
+ async deferUntilEnd(pool, work) {
19132
+ const outer = this.held(pool);
19133
+ if (outer === null) {
19134
+ await work();
19135
+ return false;
19136
+ }
19137
+ (outer.root ?? outer).afterEnd.push(work);
19138
+ return true;
18836
19139
  }
18837
19140
  async run(pool, marker, fn) {
18838
19141
  try {
@@ -18870,6 +19173,15 @@ function collectPostCommitEffects(tx) {
18870
19173
  pendingByTx.set(tx, pending);
18871
19174
  return pending;
18872
19175
  }
19176
+ function mergeSavepointEffects(effects, parentTx) {
19177
+ if (effects.length === 0)
19178
+ return;
19179
+ const pending = pendingByTx.get(parentTx);
19180
+ if (pending === undefined) {
19181
+ throw new Error(`afterCommit(${effects[0]?.label}): the transaction this call joined collects no post-commit effects`);
19182
+ }
19183
+ pending.push(...effects);
19184
+ }
18873
19185
  async function runPostCommitEffects(effects) {
18874
19186
  const failures = [];
18875
19187
  for (const effect of effects) {
@@ -18894,6 +19206,10 @@ function notifyOperationCommitted(operationName) {
18894
19206
  }
18895
19207
  }
18896
19208
  }
19209
+ var commitNotice = null;
19210
+ function commitNoticeOf(operationName) {
19211
+ return commitNotice === null ? null : commitNotice(operationName);
19212
+ }
18897
19213
 
18898
19214
  // ../query-api/src/adapter.ts
18899
19215
  var SQL3 = globalThis.Bun?.SQL;
@@ -18932,13 +19248,21 @@ class AdminDatabaseUnavailableError extends Error {
18932
19248
  function resolvePoolMax(explicit) {
18933
19249
  if (explicit !== undefined)
18934
19250
  return validatePoolMax(explicit, "poolMax option");
18935
- const raw = process.env.CAELO_DB_POOL_MAX;
19251
+ const raw = process.env[DB_POOL_MAX_ENV];
18936
19252
  if (raw === undefined || raw === "")
18937
19253
  return;
18938
- return validatePoolMax(Number(raw), "CAELO_DB_POOL_MAX");
19254
+ return validatePoolMax(Number(raw), DB_POOL_MAX_ENV);
19255
+ }
19256
+ function resolvePublicPoolMax(explicitPublic, explicitAll) {
19257
+ if (explicitPublic !== undefined)
19258
+ return validatePoolMax(explicitPublic, "publicPoolMax option");
19259
+ const raw = process.env[DB_PUBLIC_POOL_MAX_ENV];
19260
+ if (raw !== undefined && raw !== "")
19261
+ return validatePoolMax(Number(raw), DB_PUBLIC_POOL_MAX_ENV);
19262
+ return resolvePoolMax(explicitAll);
18939
19263
  }
18940
19264
  function validatePoolMax(value, source) {
18941
- if (!Number.isInteger(value) || value < 2) {
19265
+ if (!Number.isInteger(value) || value < MIN_POOL_MAX) {
18942
19266
  throw new Error(`${source} must be an integer >= 2 (got ${value}); ` + `1 self-deadlocks the pool. Leave it unset for the Bun default of 10.`);
18943
19267
  }
18944
19268
  return value;
@@ -18962,12 +19286,18 @@ function postCommitFailureMessage(failures) {
18962
19286
  const list = failures.map((f) => `${f.effect.label} (${f.message})`).join("; ");
18963
19287
  return `The change is saved, but its follow-up in the running server failed: ${list}. Do not repeat the change. Restart the admin server so the running state is rebuilt from the saved data; the failure is in the audit log.`;
18964
19288
  }
19289
+ var LISTEN_CONNECTIONS_PER_INSTANCE = 1;
18965
19290
 
18966
19291
  class DatabaseAdapter {
18967
19292
  #admin;
18968
19293
  #public;
18969
19294
  #adminRaw;
18970
19295
  #publicRaw;
19296
+ #adminUrl;
19297
+ #publicUrl;
19298
+ #listeners = new Map;
19299
+ #listenLost = new Map;
19300
+ #listenersClosing = false;
18971
19301
  #skipVerify;
18972
19302
  #roles;
18973
19303
  #acquireTimeoutMs;
@@ -18983,10 +19313,13 @@ class DatabaseAdapter {
18983
19313
  }
18984
19314
  assertPoolSafeBun(globalThis.Bun?.version ?? "0");
18985
19315
  const poolMax = resolvePoolMax(config.poolMax);
19316
+ const publicPoolMax = resolvePublicPoolMax(config.publicPoolMax, config.poolMax);
18986
19317
  const idleTimeout = config.poolIdleTimeoutSeconds ?? POOL_IDLE_TIMEOUT_SECONDS;
18987
- const open = (url) => new SQL3(url, poolMax === undefined ? { idleTimeout } : { max: poolMax, idleTimeout });
18988
- this.#adminRaw = config.adminDatabaseUrl === null ? null : open(config.adminDatabaseUrl);
18989
- this.#publicRaw = open(config.publicDatabaseUrl);
19318
+ const open = (url, max) => new SQL3(url, max === undefined ? { idleTimeout } : { max, idleTimeout });
19319
+ this.#adminRaw = config.adminDatabaseUrl === null ? null : open(config.adminDatabaseUrl, poolMax);
19320
+ this.#publicRaw = open(config.publicDatabaseUrl, publicPoolMax);
19321
+ this.#adminUrl = config.adminDatabaseUrl;
19322
+ this.#publicUrl = config.publicDatabaseUrl;
18990
19323
  this.#onStatement = config.onStatement ?? null;
18991
19324
  const observe = this.#onStatement;
18992
19325
  this.#drizzleOptions = observe ? { logger: { logQuery: (query) => observe(query) } } : {};
@@ -18999,22 +19332,64 @@ class DatabaseAdapter {
18999
19332
  const raw = pool === "admin" ? this.#adminRaw : this.#publicRaw;
19000
19333
  if (raw === null)
19001
19334
  throw new AdminDatabaseUnavailableError(holder);
19002
- const marker = this.#guard.enter(pool, holder);
19335
+ const plan = this.#guard.plan(pool, holder);
19336
+ if (plan.kind === "join")
19337
+ return this.#joinHeld(pool, plan.outer, plan.marker, fn);
19338
+ const marker = plan.marker;
19003
19339
  const reserved = await this.#reserve(pool, raw, holder);
19004
19340
  try {
19005
19341
  const db = drizzle({ client: reserved, ...this.#drizzleOptions });
19006
19342
  this.#onStatement?.("BEGIN");
19007
19343
  const out = await db.transaction((tx) => this.#guard.run(pool, marker, async () => {
19344
+ marker.tx = tx;
19008
19345
  if (exemptFromSessionTimeouts(holder))
19009
19346
  await liftSessionTimeouts(tx);
19010
- return fn(tx);
19347
+ return fn(tx, false);
19011
19348
  }));
19012
19349
  this.#onStatement?.("COMMIT");
19013
19350
  return out;
19014
19351
  } finally {
19015
19352
  reserved.release();
19353
+ await this.#runAfterEnd(marker);
19016
19354
  }
19017
19355
  }
19356
+ #joinHeld(pool, outer, marker, fn) {
19357
+ return outer.joined.run(async () => {
19358
+ const outerTx = outer.tx;
19359
+ if (outerTx === null || !outer.active) {
19360
+ throw new NestedPoolCheckoutError(pool, outer.holder, marker.holder);
19361
+ }
19362
+ let effects = [];
19363
+ const out = await outerTx.transaction((savepoint) => this.#guard.run(pool, marker, async () => {
19364
+ marker.tx = savepoint;
19365
+ effects = collectPostCommitEffects(savepoint);
19366
+ return fn(savepoint, true);
19367
+ }));
19368
+ mergeSavepointEffects(effects, outerTx);
19369
+ if (outer.identity !== null) {
19370
+ await this.#setIdentity(outerTx, outer.identity, outer.identityOpName);
19371
+ }
19372
+ return out;
19373
+ });
19374
+ }
19375
+ async#runAfterEnd(marker) {
19376
+ for (const work of marker.afterEnd.splice(0)) {
19377
+ try {
19378
+ await work();
19379
+ } catch (thrown) {
19380
+ console.error("[query-api] work deferred until a transaction ended failed", {
19381
+ holder: marker.holder,
19382
+ error: thrown instanceof Error ? thrown.message : String(thrown)
19383
+ });
19384
+ }
19385
+ }
19386
+ }
19387
+ deferUntilHeldTransactionEnds(work) {
19388
+ return this.#guard.deferUntilEnd("admin", work);
19389
+ }
19390
+ holdsTransaction(pool) {
19391
+ return this.#guard.held(pool) !== null;
19392
+ }
19018
19393
  async#reserve(pool, raw, requester) {
19019
19394
  const deadline = new AbortController;
19020
19395
  const timer = setTimeout(() => deadline.abort(), this.#acquireTimeoutMs);
@@ -19129,10 +19504,13 @@ class DatabaseAdapter {
19129
19504
  let postCommit = [];
19130
19505
  let result;
19131
19506
  let handlerError = null;
19507
+ let joinedHolder = false;
19132
19508
  try {
19133
- result = await this.#transaction(pool, op.name, async (tx) => {
19134
- postCommit = collectPostCommitEffects(tx);
19135
- await this.#setIdentity(tx, ctx);
19509
+ result = await this.#transaction(pool, op.name, async (tx, joined) => {
19510
+ joinedHolder = joined;
19511
+ if (!joined)
19512
+ postCommit = collectPostCommitEffects(tx);
19513
+ await this.#setIdentity(tx, ctx, op.name);
19136
19514
  if (op.database === "cms_admin") {
19137
19515
  const denied = await firstInvisibleBranch(tx, namedBranches(ctx, validatedInput));
19138
19516
  if (denied !== null)
@@ -19142,7 +19520,12 @@ class DatabaseAdapter {
19142
19520
  if (!out.ok)
19143
19521
  handlerError = out;
19144
19522
  if (out.ok && op.database === "cms_admin") {
19145
- publishAfterCommit = await this.#projectionDirty(tx);
19523
+ if (!joined)
19524
+ publishAfterCommit = await this.#projectionDirty(tx);
19525
+ const notice = commitNoticeOf(op.name);
19526
+ if (notice !== null) {
19527
+ await tx.execute(sql`SELECT pg_notify(${notice.channel}, ${notice.payload})`);
19528
+ }
19146
19529
  }
19147
19530
  return out;
19148
19531
  });
@@ -19152,23 +19535,11 @@ class DatabaseAdapter {
19152
19535
  if (thrown instanceof OperationAbortError) {
19153
19536
  return err(thrown.queryError);
19154
19537
  }
19155
- if (isRlsDenial(thrown)) {
19156
- return err({
19157
- kind: "RLSDenied",
19158
- operation: op.name,
19159
- detail: thrown.message ?? "RLS policy denied the write"
19160
- });
19161
- }
19162
19538
  const pgDetail = extractPgFields(thrown);
19163
19539
  if (pgDetail?.code === COMMIT_ROLLED_BACK && handlerError !== null) {
19164
19540
  return handlerError;
19165
19541
  }
19166
- return err({
19167
- kind: "HandlerError",
19168
- operation: op.name,
19169
- message: thrown instanceof Error ? thrown.message : String(thrown),
19170
- ...pgDetail ? { pgDetail } : {}
19171
- });
19542
+ return err(thrownToQueryError(op.name, thrown, "write"));
19172
19543
  }
19173
19544
  const effectFailures = await this.#settlePostCommit(op.name, ctx, postCommit);
19174
19545
  let publishFailure = null;
@@ -19179,7 +19550,7 @@ class DatabaseAdapter {
19179
19550
  publishFailure = `The change is saved, but the public site does not serve it yet: publishing it failed (${thrown instanceof Error ? thrown.message : String(thrown)}). Do not repeat the change; it is published automatically within seconds once the public database answers.`;
19180
19551
  }
19181
19552
  }
19182
- if (result.ok)
19553
+ if (result.ok && !joinedHolder)
19183
19554
  notifyOperationCommitted(op.name);
19184
19555
  if (effectFailures.length > 0) {
19185
19556
  const effects = postCommitFailureMessage(effectFailures);
@@ -19213,22 +19584,77 @@ class DatabaseAdapter {
19213
19584
  throw thrown;
19214
19585
  if (thrown instanceof OperationAbortError)
19215
19586
  return err(thrown.queryError);
19216
- const pgDetail = extractPgFields(thrown);
19217
- return err({
19218
- kind: "HandlerError",
19219
- operation: op.name,
19220
- message: thrown instanceof Error ? thrown.message : String(thrown),
19221
- ...pgDetail ? { pgDetail } : {}
19222
- });
19587
+ return err(thrownToQueryError(op.name, thrown, "write"));
19223
19588
  }
19224
19589
  }
19225
- async#setIdentity(tx, ctx) {
19590
+ async#setIdentity(tx, ctx, opName = null) {
19226
19591
  await tx.execute(sql`SELECT
19227
19592
  set_config('caelo.actor_id', ${ctx.actorId}, true),
19228
19593
  set_config('caelo.actor_kind', ${ctx.actorKind}, true),
19229
19594
  set_config('caelo.plugin_id', ${ctx.pluginId ?? ""}, true),
19230
19595
  set_config('caelo.chat_branch_id', ${ctx.chatBranchId ?? ""}, true),
19231
- set_config('caelo.chat_task_id', ${ctx.chatTaskId ?? ""}, true)`);
19596
+ set_config('caelo.chat_task_id', ${ctx.chatTaskId ?? ""}, true),
19597
+ set_config('caelo.op_name', ${opName ?? ""}, true)`);
19598
+ this.#recordIdentity(tx, ctx, opName);
19599
+ }
19600
+ async#setPublicIdentity(tx, ctx) {
19601
+ await tx.execute(sql`SELECT
19602
+ set_config('caelo.actor_id', ${ctx.actorId}, true),
19603
+ set_config('caelo.actor_kind', ${ctx.actorKind}, true),
19604
+ set_config('caelo.plugin_id', ${ctx.pluginId ?? ""}, true)`);
19605
+ this.#recordIdentity(tx, ctx, null);
19606
+ }
19607
+ #recordIdentity(tx, ctx, opName) {
19608
+ for (const pool of ["admin", "public"]) {
19609
+ const marker = this.#guard.held(pool);
19610
+ if (marker !== null && marker.tx === tx) {
19611
+ marker.identity = ctx;
19612
+ marker.identityOpName = opName;
19613
+ }
19614
+ }
19615
+ }
19616
+ async listen(pool, channel, callbacks) {
19617
+ const url = pool === "admin" ? this.#adminUrl : this.#publicUrl;
19618
+ if (url === null)
19619
+ throw new AdminDatabaseUnavailableError(`listen(${channel})`);
19620
+ let lost = this.#listenLost.get(pool);
19621
+ if (!lost) {
19622
+ lost = new Set;
19623
+ this.#listenLost.set(pool, lost);
19624
+ }
19625
+ const lostSet = lost;
19626
+ let conn = this.#listeners.get(pool);
19627
+ if (!conn) {
19628
+ conn = new SQL3(url, {
19629
+ max: LISTEN_CONNECTIONS_PER_INSTANCE,
19630
+ onclose: (cause) => {
19631
+ if (this.#listenersClosing)
19632
+ return;
19633
+ for (const onLost of lostSet) {
19634
+ try {
19635
+ onLost(cause);
19636
+ } catch (thrown) {
19637
+ console.error(`[query-api] a LISTEN onLost callback failed`, thrown);
19638
+ }
19639
+ }
19640
+ }
19641
+ });
19642
+ this.#listeners.set(pool, conn);
19643
+ }
19644
+ const onLost = callbacks.onLost;
19645
+ lostSet.add(onLost);
19646
+ const handle = await conn.listen(channel, callbacks.onNotify, callbacks.onListen);
19647
+ return {
19648
+ unlisten: async () => {
19649
+ lostSet.delete(onLost);
19650
+ await handle.unlisten();
19651
+ }
19652
+ };
19653
+ }
19654
+ async notify(channel, payload) {
19655
+ await this.#transaction("admin", "notify", async (tx) => {
19656
+ await tx.execute(sql`SELECT pg_notify(${channel}, ${payload})`);
19657
+ });
19232
19658
  }
19233
19659
  async#runReadOperation(op, ctx, validatedInput) {
19234
19660
  try {
@@ -19244,20 +19670,7 @@ class DatabaseAdapter {
19244
19670
  throw thrown;
19245
19671
  if (thrown instanceof OperationAbortError)
19246
19672
  return err(thrown.queryError);
19247
- if (isRlsDenial(thrown)) {
19248
- return err({
19249
- kind: "RLSDenied",
19250
- operation: op.name,
19251
- detail: thrown.message ?? "RLS policy denied the read"
19252
- });
19253
- }
19254
- const pgDetail = extractPgFields(thrown);
19255
- return err({
19256
- kind: "HandlerError",
19257
- operation: op.name,
19258
- message: thrown instanceof Error ? thrown.message : String(thrown),
19259
- ...pgDetail ? { pgDetail } : {}
19260
- });
19673
+ return err(thrownToQueryError(op.name, thrown, "read"));
19261
19674
  }
19262
19675
  }
19263
19676
  readScope(ctx, holder = "reads.read") {
@@ -19306,7 +19719,14 @@ class DatabaseAdapter {
19306
19719
  return failures;
19307
19720
  }
19308
19721
  async close() {
19309
- await Promise.all([this.#adminRaw?.end(), this.#publicRaw.end()]);
19722
+ this.#listenersClosing = true;
19723
+ const listeners = [...this.#listeners.values()];
19724
+ this.#listeners.clear();
19725
+ await Promise.all([
19726
+ this.#adminRaw?.end(),
19727
+ this.#publicRaw.end(),
19728
+ ...listeners.map((l) => l.close())
19729
+ ]);
19310
19730
  }
19311
19731
  async withAdminTransaction(ctx, fn, options = {}) {
19312
19732
  const holder = options.holder ?? "withAdminTransaction";
@@ -19316,15 +19736,17 @@ class DatabaseAdapter {
19316
19736
  await this.verifyRoles();
19317
19737
  let publishAfterCommit = false;
19318
19738
  let postCommit = [];
19319
- const out = await this.#transaction("admin", holder, async (tx) => {
19320
- postCommit = collectPostCommitEffects(tx);
19321
- await this.#setIdentity(tx, ctx);
19739
+ const out = await this.#transaction("admin", holder, async (tx, joined) => {
19740
+ if (!joined)
19741
+ postCommit = collectPostCommitEffects(tx);
19742
+ await this.#setIdentity(tx, ctx, options.holder ?? null);
19322
19743
  const denied = await firstInvisibleBranch(tx, namedBranches(ctx, undefined));
19323
19744
  if (denied !== null) {
19324
19745
  throw new OperationAbortError(branchNotFound("withAdminTransaction", denied));
19325
19746
  }
19326
19747
  const result = await fn(tx);
19327
- publishAfterCommit = await this.#projectionDirty(tx);
19748
+ if (!joined)
19749
+ publishAfterCommit = await this.#projectionDirty(tx);
19328
19750
  return result;
19329
19751
  });
19330
19752
  const effectFailures = await this.#settlePostCommit(holder, ctx, postCommit);
@@ -19340,11 +19762,10 @@ class DatabaseAdapter {
19340
19762
  if (!this.#skipVerify)
19341
19763
  await this.verifyRoles();
19342
19764
  let postCommit = [];
19343
- const out = await this.#transaction("public", holder, async (tx) => {
19344
- postCommit = collectPostCommitEffects(tx);
19345
- await tx.execute(sql`SELECT set_config('caelo.actor_id', ${ctx.actorId}, true)`);
19346
- await tx.execute(sql`SELECT set_config('caelo.actor_kind', ${ctx.actorKind}, true)`);
19347
- await tx.execute(sql`SELECT set_config('caelo.plugin_id', ${ctx.pluginId ?? ""}, true)`);
19765
+ const out = await this.#transaction("public", holder, async (tx, joined) => {
19766
+ if (!joined)
19767
+ postCommit = collectPostCommitEffects(tx);
19768
+ await this.#setPublicIdentity(tx, ctx);
19348
19769
  return await fn(tx);
19349
19770
  });
19350
19771
  const effectFailures = await this.#settlePostCommit(holder, ctx, postCommit);
@@ -19355,11 +19776,11 @@ class DatabaseAdapter {
19355
19776
  rawAdmin() {
19356
19777
  if (this.#adminRaw === null)
19357
19778
  throw new AdminDatabaseUnavailableError("rawAdmin");
19358
- this.#guard.assertMayCheckout("admin", "rawAdmin()");
19779
+ this.#guard.assertNoHeldTransaction("admin", "rawAdmin()");
19359
19780
  return this.#adminRaw;
19360
19781
  }
19361
19782
  rawPublic() {
19362
- this.#guard.assertMayCheckout("public", "rawPublic()");
19783
+ this.#guard.assertNoHeldTransaction("public", "rawPublic()");
19363
19784
  return this.#publicRaw;
19364
19785
  }
19365
19786
  async provisionPluginPublicSchema(opts) {
@@ -19386,7 +19807,10 @@ class DatabaseAdapter {
19386
19807
  if (!this.#skipVerify)
19387
19808
  await this.verifyRoles();
19388
19809
  await this.#transaction("admin", "provisionPluginAdminSchema", async (tx) => {
19389
- const outcome = await provisionPluginSchemaInTx(tx, "provisionPluginAdminSchema", opts);
19810
+ const outcome = await provisionPluginSchemaInTx(tx, "provisionPluginAdminSchema", {
19811
+ ...opts,
19812
+ journal: true
19813
+ });
19390
19814
  if (outcome.droppedColumns.length > 0) {
19391
19815
  await auditDroppedColumns(tx, "cms_admin", opts.pluginId, outcome);
19392
19816
  }
@@ -19419,11 +19843,11 @@ class DatabaseAdapter {
19419
19843
  }
19420
19844
  get admin() {
19421
19845
  const db = this.#adminDb("admin pool");
19422
- this.#guard.assertMayCheckout("admin", "adapter.admin");
19846
+ this.#guard.assertNoHeldTransaction("admin", "adapter.admin");
19423
19847
  return db;
19424
19848
  }
19425
19849
  get public() {
19426
- this.#guard.assertMayCheckout("public", "adapter.public");
19850
+ this.#guard.assertNoHeldTransaction("public", "adapter.public");
19427
19851
  return this.#public;
19428
19852
  }
19429
19853
  }
@@ -19437,6 +19861,39 @@ async function auditDroppedColumns(tx, database, pluginId, outcome) {
19437
19861
  resultSummary: `${database}.${outcome.schemaName}: dropped ${outcome.droppedColumns.join(", ")}`
19438
19862
  });
19439
19863
  }
19864
+ function thrownToQueryError(operation, thrown, access) {
19865
+ const raw = thrown instanceof Error ? thrown.message : String(thrown);
19866
+ if (isRlsDenial(thrown)) {
19867
+ console.warn(`[query-api] ${operation}: RLS denied the ${access}: ${raw}`);
19868
+ return { kind: "RLSDenied", operation, detail: rlsDenialMessage(operation, access) };
19869
+ }
19870
+ const pgDetail = extractPgFields(thrown);
19871
+ if (pgDetail?.code) {
19872
+ const described = describePgFailure(pgDetail);
19873
+ if (described.internal)
19874
+ console.error(`[query-api] ${operation} failed: ${raw}`, pgDetail);
19875
+ return {
19876
+ kind: "HandlerError",
19877
+ operation,
19878
+ message: described.message,
19879
+ pgDetail,
19880
+ ...described.internal ? { internal: true } : {}
19881
+ };
19882
+ }
19883
+ return {
19884
+ kind: "HandlerError",
19885
+ operation,
19886
+ message: raw,
19887
+ internal: true,
19888
+ ...pgDetail ? { pgDetail } : {}
19889
+ };
19890
+ }
19891
+ // ../query-api/src/change-journal.ts
19892
+ function textArray(values) {
19893
+ return sql`ARRAY(SELECT jsonb_array_elements_text((${JSON.stringify(values)}::text)::jsonb))`;
19894
+ }
19895
+ var JOURNAL_RETENTION_MS = 24 * 60 * 60 * 1000;
19896
+ var JOURNAL_STALLED_CONSUMER_MS = 7 * 24 * 60 * 60 * 1000;
19440
19897
  // ../query-api/src/execute.ts
19441
19898
  var allowAllRateLimiter = {
19442
19899
  async check() {
@@ -19472,8 +19929,15 @@ async function execute(registry, adapter, ctx, name, rawInput, options = {}) {
19472
19929
  adapter,
19473
19930
  ...options.signal ? { signal: options.signal } : {}
19474
19931
  });
19475
- if (result.ok)
19932
+ if (result.ok) {
19476
19933
  notifyOperationCommitted(op.name);
19934
+ const notice = commitNoticeOf(op.name);
19935
+ if (notice !== null) {
19936
+ await adapter.notify(notice.channel, notice.payload).catch((thrown) => {
19937
+ console.error(`[query-api] could not notify other instances of ${op.name}`, thrown);
19938
+ });
19939
+ }
19940
+ }
19477
19941
  return result;
19478
19942
  }
19479
19943
  return adapter.runOperation(op, ctx, parsed.data);
@@ -19537,6 +20001,7 @@ var claimOutput = object({
19537
20001
  });
19538
20002
  var jobClaimOp = defineOperation({
19539
20003
  name: "platform_updates.job_claim",
20004
+ readScope: "request",
19540
20005
  actorScope: ["system"],
19541
20006
  database: "cms_admin",
19542
20007
  input: object({
@@ -19637,6 +20102,7 @@ var jobClaimOp = defineOperation({
19637
20102
  });
19638
20103
  var jobHandoverOp = defineOperation({
19639
20104
  name: "platform_updates.job_handover",
20105
+ readScope: "request",
19640
20106
  actorScope: ["system"],
19641
20107
  database: "cms_admin",
19642
20108
  input: object({
@@ -19667,6 +20133,7 @@ var jobHandoverOp = defineOperation({
19667
20133
  });
19668
20134
  var jobEventOp = defineOperation({
19669
20135
  name: "platform_updates.job_event",
20136
+ readScope: "request",
19670
20137
  actorScope: ["system"],
19671
20138
  database: "cms_admin",
19672
20139
  input: object({
@@ -19687,6 +20154,7 @@ var jobEventOp = defineOperation({
19687
20154
  var digest2 = string2().regex(/^sha256:[0-9a-f]{64}$/);
19688
20155
  var jobRecordOp = defineOperation({
19689
20156
  name: "platform_updates.job_record",
20157
+ readScope: "request",
19690
20158
  actorScope: ["system"],
19691
20159
  database: "cms_admin",
19692
20160
  input: object({
@@ -19722,6 +20190,7 @@ var jobRecordOp = defineOperation({
19722
20190
  });
19723
20191
  var jobFinishOp = defineOperation({
19724
20192
  name: "platform_updates.job_finish",
20193
+ readScope: "request",
19725
20194
  actorScope: ["system"],
19726
20195
  database: "cms_admin",
19727
20196
  input: object({
@@ -19733,7 +20202,7 @@ var jobFinishOp = defineOperation({
19733
20202
  }).strict(),
19734
20203
  output: object({ finished: boolean2() }),
19735
20204
  handler: async (ctx, input, tx) => {
19736
- const codes = input.codes.length > 0 ? sql`ARRAY[${sql.join(input.codes.map((c) => sql`${c}`), sql`, `)}]::text[]` : sql`'{}'::text[]`;
20205
+ const codes = textArray(input.codes);
19737
20206
  const rows = await tx.execute(sql`
19738
20207
  UPDATE platform_update_requests
19739
20208
  SET status = ${input.status}, status_reason = ${input.reason},
@@ -19763,6 +20232,7 @@ var jobFinishOp = defineOperation({
19763
20232
  });
19764
20233
  var agentHeartbeatOp = defineOperation({
19765
20234
  name: "platform_updates.agent_heartbeat",
20235
+ readScope: "request",
19766
20236
  actorScope: ["system"],
19767
20237
  database: "cms_admin",
19768
20238
  input: object({