latticesql 3.4.0 → 3.4.1

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/cli.js CHANGED
@@ -1637,43 +1637,57 @@ var init_postgres = __esm({
1637
1637
  },
1638
1638
  {
1639
1639
  warn: "could not register json_extract polyfill:",
1640
- sql: `CREATE OR REPLACE FUNCTION json_extract(doc text, path text)
1641
- RETURNS text
1642
- LANGUAGE sql
1643
- IMMUTABLE
1644
- AS $fn$
1645
- SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
1646
- $fn$;`
1640
+ // Create ONLY if absent. `CREATE OR REPLACE` on an existing function requires
1641
+ // ownership, but on a cloud the function is owned by whichever single role
1642
+ // created it first — so every OTHER member's per-connect replace raised "must
1643
+ // be owner of function" and (sharing the render transaction) aborted it,
1644
+ // yielding an empty render. The IF-absent guard makes a present function a
1645
+ // clean no-op for everyone, regardless of who owns it.
1646
+ sql: `DO $do$ BEGIN
1647
+ IF to_regprocedure('json_extract(text, text)') IS NULL THEN
1648
+ CREATE FUNCTION json_extract(doc text, path text)
1649
+ RETURNS text
1650
+ LANGUAGE sql
1651
+ IMMUTABLE
1652
+ AS $fn$
1653
+ SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
1654
+ $fn$;
1655
+ END IF;
1656
+ END $do$;`
1647
1657
  },
1648
1658
  {
1649
1659
  warn: "could not register strftime polyfill:",
1650
- sql: `CREATE OR REPLACE FUNCTION strftime(format text, modifier text)
1651
- RETURNS text
1652
- LANGUAGE plpgsql
1653
- IMMUTABLE
1654
- AS $fn$
1655
- DECLARE ts timestamptz;
1656
- BEGIN
1657
- IF modifier = 'now' THEN
1658
- ts := now();
1659
- ELSE
1660
- ts := modifier::timestamptz;
1661
- END IF;
1662
- RETURN to_char(
1663
- ts AT TIME ZONE 'UTC',
1664
- replace(replace(replace(replace(replace(replace(replace(replace(
1665
- format,
1666
- '%Y', 'YYYY'),
1667
- '%m', 'MM'),
1668
- '%d', 'DD'),
1669
- '%H', 'HH24'),
1670
- '%M', 'MI'),
1671
- '%S', 'SS'),
1672
- '%f', 'MS'),
1673
- 'T', '"T"')
1674
- );
1675
- END;
1676
- $fn$;`
1660
+ sql: `DO $do$ BEGIN
1661
+ IF to_regprocedure('strftime(text, text)') IS NULL THEN
1662
+ CREATE FUNCTION strftime(format text, modifier text)
1663
+ RETURNS text
1664
+ LANGUAGE plpgsql
1665
+ IMMUTABLE
1666
+ AS $fn$
1667
+ DECLARE ts timestamptz;
1668
+ BEGIN
1669
+ IF modifier = 'now' THEN
1670
+ ts := now();
1671
+ ELSE
1672
+ ts := modifier::timestamptz;
1673
+ END IF;
1674
+ RETURN to_char(
1675
+ ts AT TIME ZONE 'UTC',
1676
+ replace(replace(replace(replace(replace(replace(replace(replace(
1677
+ format,
1678
+ '%Y', 'YYYY'),
1679
+ '%m', 'MM'),
1680
+ '%d', 'DD'),
1681
+ '%H', 'HH24'),
1682
+ '%M', 'MI'),
1683
+ '%S', 'SS'),
1684
+ '%f', 'MS'),
1685
+ 'T', '"T"')
1686
+ );
1687
+ END;
1688
+ $fn$;
1689
+ END IF;
1690
+ END $do$;`
1677
1691
  }
1678
1692
  ];
1679
1693
  }
@@ -3077,6 +3091,34 @@ var init_engine = __esm({
3077
3091
  setRenderFold(fn) {
3078
3092
  this._foldRows = fn;
3079
3093
  }
3094
+ /**
3095
+ * Incremental scope: is this entity-context table affected by a change to one
3096
+ * of `changed`? Affected when the table itself changed (its own rows / `self`
3097
+ * source / index) OR any of its files SOURCES from a changed table (a cross-
3098
+ * table dependent — e.g. an AGENT.md that lists the agent's tasks must re-render
3099
+ * when `tasks` changes). A `custom` source runs an arbitrary query, so we can't
3100
+ * prove independence — treat it as always-affected (conservative, never stale).
3101
+ */
3102
+ _entityAffected(table, def, changed) {
3103
+ if (changed.has(table)) return true;
3104
+ for (const spec of Object.values(def.files)) {
3105
+ if (this._sourceTouches(spec.source, changed)) return true;
3106
+ }
3107
+ return false;
3108
+ }
3109
+ _sourceTouches(source, changed) {
3110
+ if (source == null || typeof source !== "object") return false;
3111
+ const s2 = source;
3112
+ if (s2.type === "custom") return true;
3113
+ if (typeof s2.table === "string" && changed.has(s2.table)) return true;
3114
+ if (typeof s2.junctionTable === "string" && changed.has(s2.junctionTable)) return true;
3115
+ if (s2.sources != null && typeof s2.sources === "object") {
3116
+ for (const sub of Object.values(s2.sources)) {
3117
+ if (this._sourceTouches(sub, changed)) return true;
3118
+ }
3119
+ }
3120
+ return false;
3121
+ }
3080
3122
  async render(outputDir, opts = {}) {
3081
3123
  const start = Date.now();
3082
3124
  const filesWritten = [];
@@ -3086,6 +3128,7 @@ var init_engine = __esm({
3086
3128
  for (const [name, def] of this._schema.getTables()) {
3087
3129
  if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
3088
3130
  if (this._skipEmpty && def.render === NOOP_RENDER) continue;
3131
+ if (opts.changedTables && !opts.changedTables.has(name)) continue;
3089
3132
  let rows = await this._schema.queryTable(this._adapter, name, this._readRel);
3090
3133
  if (def.relevanceFilter) {
3091
3134
  const ctx = this._getTaskContext();
@@ -3138,6 +3181,9 @@ var init_engine = __esm({
3138
3181
  }
3139
3182
  for (const [name, def] of this._schema.getMultis()) {
3140
3183
  if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
3184
+ if (opts.changedTables && def.tables && !def.tables.some((t8) => opts.changedTables?.has(t8))) {
3185
+ continue;
3186
+ }
3141
3187
  const keys = await def.keys();
3142
3188
  const tables = {};
3143
3189
  if (def.tables) {
@@ -3169,16 +3215,22 @@ var init_engine = __esm({
3169
3215
  filesWritten,
3170
3216
  counters,
3171
3217
  throttle,
3172
- signal
3218
+ signal,
3219
+ opts.changedTables
3173
3220
  );
3174
3221
  if (entityContextManifest === null) {
3175
3222
  return this._abortedResult(filesWritten, counters, start);
3176
3223
  }
3177
3224
  if (this._schema.getEntityContexts().size > 0) {
3225
+ let entityContexts = entityContextManifest;
3226
+ if (opts.changedTables) {
3227
+ const prev = readManifest(outputDir);
3228
+ entityContexts = { ...prev?.entityContexts ?? {}, ...entityContextManifest };
3229
+ }
3178
3230
  writeManifest(outputDir, {
3179
3231
  version: 2,
3180
3232
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
3181
- entityContexts: entityContextManifest
3233
+ entityContexts
3182
3234
  });
3183
3235
  }
3184
3236
  const result = {
@@ -3244,12 +3296,14 @@ var init_engine = __esm({
3244
3296
  * partial tree). Progress is reported through `throttle`; abort is observed
3245
3297
  * via `signal`.
3246
3298
  */
3247
- async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
3299
+ async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables) {
3248
3300
  const protectedTables = /* @__PURE__ */ new Set();
3249
3301
  for (const [t8, d6] of this._schema.getEntityContexts()) {
3250
3302
  if (d6.protected) protectedTables.add(t8);
3251
3303
  }
3252
- const entityTables = [...this._schema.getEntityContexts()];
3304
+ const entityTables = [...this._schema.getEntityContexts()].filter(
3305
+ ([table, def]) => !changedTables || this._entityAffected(table, def, changedTables)
3306
+ );
3253
3307
  const tableCount = entityTables.length;
3254
3308
  if (signal?.aborted) return null;
3255
3309
  const renderedEntries = await mapWithConcurrency(
@@ -5077,6 +5131,16 @@ var init_lattice = __esm({
5077
5131
  _autoRenderPending = false;
5078
5132
  _autoRenderInFlight = false;
5079
5133
  _autoRenderDebounceMs = 250;
5134
+ /**
5135
+ * Incremental auto-render scope, accumulated between debounced renders. A write
5136
+ * or a remote (cloud) change records the AFFECTED table here, so the next
5137
+ * auto-render re-renders only that entity (+ its cross-table dependents) instead
5138
+ * of the whole tree. `_pendingRenderAll` forces a full render (the initial
5139
+ * render, or a change with no known table). Captured + reset when a render
5140
+ * starts, so changes during a render re-accumulate and re-trigger.
5141
+ */
5142
+ _pendingRenderTables = /* @__PURE__ */ new Set();
5143
+ _pendingRenderAll = true;
5080
5144
  /** Cache of actual table columns (from PRAGMA), populated after init(). */
5081
5145
  _columnCache = /* @__PURE__ */ new Map();
5082
5146
  /** Derived encryption key (from options.encryptionKey via scrypt). */
@@ -6172,7 +6236,7 @@ var init_lattice = __esm({
6172
6236
  `${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`,
6173
6237
  Object.values(filtered)
6174
6238
  );
6175
- this._scheduleAutoRender();
6239
+ this._scheduleAutoRender(junctionTable);
6176
6240
  }
6177
6241
  /**
6178
6242
  * Delete rows from a junction table matching all given conditions.
@@ -6188,7 +6252,7 @@ var init_lattice = __esm({
6188
6252
  `DELETE FROM "${junctionTable}" WHERE ${where}`,
6189
6253
  entries.map(([, v2]) => v2)
6190
6254
  );
6191
- this._scheduleAutoRender();
6255
+ this._scheduleAutoRender(junctionTable);
6192
6256
  }
6193
6257
  // -------------------------------------------------------------------------
6194
6258
  // Seeding DSL (v0.13+)
@@ -6444,6 +6508,11 @@ var init_lattice = __esm({
6444
6508
  async renderInBackground(outputDir, opts = {}) {
6445
6509
  const notInit = this._notInitError();
6446
6510
  if (notInit) return notInit;
6511
+ if (!opts.changedTables) {
6512
+ this._pendingRenderAll = false;
6513
+ this._pendingRenderTables = /* @__PURE__ */ new Set();
6514
+ this._autoRenderPending = false;
6515
+ }
6447
6516
  return this._renderGuarded(outputDir, opts);
6448
6517
  }
6449
6518
  /**
@@ -6473,9 +6542,12 @@ var init_lattice = __esm({
6473
6542
  * tree when a REMOTE change arrives — notably an owner re-sharing or un-sharing
6474
6543
  * a row, after which the member's per-viewer projection must be recompiled. A
6475
6544
  * no-op when auto-render isn't enabled.
6545
+ *
6546
+ * Pass the CHANGED table so only that entity (+ its cross-table dependents) is
6547
+ * re-rendered instead of the whole tree; omit it to force a full render.
6476
6548
  */
6477
- requestRender() {
6478
- this._scheduleAutoRender();
6549
+ requestRender(table) {
6550
+ this._scheduleAutoRender(table);
6479
6551
  }
6480
6552
  /**
6481
6553
  * True while a render is actively writing the context tree + manifest (auto-
@@ -6876,7 +6948,7 @@ var init_lattice = __esm({
6876
6948
  for (const h6 of this._errorHandlers) h6(err instanceof Error ? err : new Error(String(err)));
6877
6949
  }
6878
6950
  }
6879
- this._scheduleAutoRender();
6951
+ this._scheduleAutoRender(table);
6880
6952
  }
6881
6953
  /**
6882
6954
  * Turn on automatic rendering into `outputDir`. After this, every insert /
@@ -6900,10 +6972,18 @@ var init_lattice = __esm({
6900
6972
  this._autoRenderPending = false;
6901
6973
  return this;
6902
6974
  }
6903
- _scheduleAutoRender() {
6975
+ _scheduleAutoRender(table) {
6904
6976
  if (!this._autoRenderDir) return;
6977
+ if (table === void 0) this._pendingRenderAll = true;
6978
+ else this._pendingRenderTables.add(table);
6905
6979
  this._autoRenderPending = true;
6906
- if (this._autoRenderTimer) return;
6980
+ this._armAutoRenderTimer();
6981
+ }
6982
+ /** Arm the debounce timer if not already armed. Does NOT change the render
6983
+ * scope — used both by `_scheduleAutoRender` and the post-render re-arm so a
6984
+ * re-arm never escalates a pending incremental render to a full one. */
6985
+ _armAutoRenderTimer() {
6986
+ if (!this._autoRenderDir || this._autoRenderTimer) return;
6907
6987
  this._autoRenderTimer = setTimeout(() => {
6908
6988
  this._autoRenderTimer = void 0;
6909
6989
  void this._runAutoRender();
@@ -6943,10 +7023,15 @@ var init_lattice = __esm({
6943
7023
  }
6944
7024
  if (!this._autoRenderPending) return;
6945
7025
  this._autoRenderPending = false;
7026
+ const renderAll = this._pendingRenderAll;
7027
+ const changed = this._pendingRenderTables;
7028
+ this._pendingRenderAll = false;
7029
+ this._pendingRenderTables = /* @__PURE__ */ new Set();
6946
7030
  this._autoRenderInFlight = true;
6947
7031
  try {
6948
7032
  const prevManifest = readManifest(dir);
6949
- const result = await this._render.render(dir);
7033
+ const renderOpts = renderAll || changed.size === 0 ? {} : { changedTables: changed };
7034
+ const result = await this._render.render(dir, renderOpts);
6950
7035
  for (const h6 of this._renderHandlers) h6(result);
6951
7036
  const newManifest = readManifest(dir);
6952
7037
  await this._render.cleanup(dir, prevManifest, {}, newManifest);
@@ -6958,7 +7043,7 @@ var init_lattice = __esm({
6958
7043
  }
6959
7044
  }
6960
7045
  _rearmAutoRenderIfPending() {
6961
- if (this._autoRenderPending) this._scheduleAutoRender();
7046
+ if (this._autoRenderPending) this._armAutoRenderTimer();
6962
7047
  }
6963
7048
  /**
6964
7049
  * Update or remove the embedding for a row.
@@ -8035,6 +8120,17 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
8035
8120
  FOR EACH ROW EXECUTE FUNCTION "${trg}"();
8036
8121
  `;
8037
8122
  }
8123
+ async function ownPolyfillsByGroup(db) {
8124
+ if (!isPg(db)) return;
8125
+ for (const sig of ["json_extract(text, text)", "strftime(text, text)"]) {
8126
+ try {
8127
+ const reg = await getAsyncOrSync(db.adapter, `SELECT to_regprocedure($1) AS reg`, [sig]);
8128
+ if (reg?.reg == null) continue;
8129
+ await runAsyncOrSync(db.adapter, `ALTER FUNCTION ${sig} OWNER TO "${MEMBER_GROUP}"`);
8130
+ } catch {
8131
+ }
8132
+ }
8133
+ }
8038
8134
  async function installCloudRls(db) {
8039
8135
  if (!isPg(db)) return;
8040
8136
  const schema = await cloudSchema(db);
@@ -8068,6 +8164,24 @@ CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH C
8068
8164
  `
8069
8165
  );
8070
8166
  }
8167
+ async function enableChatPrivacyRls(db) {
8168
+ if (!isPg(db)) return;
8169
+ for (const t8 of ["chat_threads", "chat_messages"]) {
8170
+ const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [t8]);
8171
+ if (reg?.reg == null) continue;
8172
+ const q3 = `"${t8}"`;
8173
+ await runCloudBootstrapSql(
8174
+ db,
8175
+ `
8176
+ ALTER TABLE ${q3} ENABLE ROW LEVEL SECURITY;
8177
+ ALTER TABLE ${q3} FORCE ROW LEVEL SECURITY;
8178
+ DROP POLICY IF EXISTS "lattice_chat_owner" ON ${q3};
8179
+ CREATE POLICY "lattice_chat_owner" ON ${q3} AS RESTRICTIVE FOR SELECT
8180
+ USING ("owner_user_id" IS NOT NULL AND "owner_user_id" = session_user);
8181
+ `
8182
+ );
8183
+ }
8184
+ }
8071
8185
  async function enableRlsForTable(db, table, pkCols) {
8072
8186
  if (!isPg(db)) return;
8073
8187
  const schema = await cloudSchema(db);
@@ -55860,12 +55974,17 @@ var appJs = `
55860
55974
  fetchJson('/api/gui-meta/columns').catch(function () { return {}; }),
55861
55975
  fetchJson('/api/system-tables').catch(function () { return { tables: [] }; }),
55862
55976
  fetchJson('/api/workspaces').catch(function () { return null; }),
55977
+ fetchJson('/api/dbconfig').catch(function () { return {}; }),
55863
55978
  ]).then(function (results) {
55864
55979
  state.entities = results[0];
55865
55980
  state.iconOverrides = results[1] || {};
55866
55981
  state.columnMeta = results[2] || {};
55867
55982
  state.systemTables = (results[3] && results[3].tables) || [];
55868
55983
  renderWsSwitcher(results[4]);
55984
+ // Re-point the header logo at the NEW workspace's mark \u2014 the switch path
55985
+ // must refresh branding the way boot does (the etag cache-busts the
55986
+ // <img>), else the previous workspace's logo stays until a hard refresh.
55987
+ applyWorkspaceLogo((results[5] || {}).logoEtag);
55869
55988
  renderSidebar();
55870
55989
  // renderWsSwitcher set cloudMode from the new workspace's kind; re-render
55871
55990
  // the composer so the Private-mode toggle reflects local vs cloud (it is
@@ -62913,9 +63032,11 @@ async function secureCloud(db) {
62913
63032
  if (db.getDialect() !== "postgres") return;
62914
63033
  await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
62915
63034
  await installCloudRls(db);
63035
+ await ownPolyfillsByGroup(db);
62916
63036
  await installCloudSettings(db);
62917
63037
  await db.ensureObservationSubstrate();
62918
63038
  await enableChangelogRls(db);
63039
+ await enableChatPrivacyRls(db);
62919
63040
  await convergeLegacyColumnAudience(db);
62920
63041
  const registered = db.getRegisteredTableNames();
62921
63042
  for (const table of registered) {
@@ -65024,6 +65145,7 @@ function activeCloudCoords(configPath) {
65024
65145
  init_assistant_routes();
65025
65146
 
65026
65147
  // src/gui/chat-routes.ts
65148
+ init_adapter();
65027
65149
  init_assistant_routes();
65028
65150
  init_chat();
65029
65151
  init_user_config();
@@ -65049,6 +65171,15 @@ function sendJson3(res, body, status = 200) {
65049
65171
  function asStr(v2, fallback = "") {
65050
65172
  return typeof v2 === "string" ? v2 : fallback;
65051
65173
  }
65174
+ function isCloudChat(db) {
65175
+ return db.getDialect() === "postgres";
65176
+ }
65177
+ async function resolveChatOwnerId(db) {
65178
+ if (!isCloudChat(db)) return null;
65179
+ const row = await getAsyncOrSync(db.adapter, "SELECT session_user AS u");
65180
+ const u2 = row?.u;
65181
+ return typeof u2 === "string" && u2.length > 0 ? u2 : null;
65182
+ }
65052
65183
  function readJson3(req) {
65053
65184
  return new Promise((resolve12, reject) => {
65054
65185
  let raw = "";
@@ -65241,12 +65372,20 @@ async function persistMessage(db, threadId, role, text, ownerUserId, turns, star
65241
65372
  });
65242
65373
  }
65243
65374
  async function dispatchChatRoute(req, res, ctx) {
65375
+ const ownerUserId = await resolveChatOwnerId(ctx.db);
65376
+ const cloud = isCloudChat(ctx.db);
65377
+ const ownedByMe = (r6) => !cloud || r6.owner_user_id != null && r6.owner_user_id === ownerUserId;
65244
65378
  if (ctx.method === "GET" && ctx.pathname === "/api/chat/threads") {
65379
+ if (cloud && ownerUserId == null) {
65380
+ sendJson3(res, { threads: [] });
65381
+ return true;
65382
+ }
65245
65383
  const filters = [
65246
65384
  { col: "deleted_at", op: "isNull" }
65247
65385
  ];
65386
+ if (ownerUserId != null) filters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
65248
65387
  const rows = await ctx.db.query("chat_threads", { filters, limit: 100 });
65249
- const threads = rows.filter((r6) => !r6.deleted_at).map((r6) => ({
65388
+ const threads = rows.filter((r6) => !r6.deleted_at && ownedByMe(r6)).map((r6) => ({
65250
65389
  id: asStr(r6.id),
65251
65390
  title: asStr(r6.title, "Chat"),
65252
65391
  created_at: asStr(r6.created_at)
@@ -65257,12 +65396,19 @@ async function dispatchChatRoute(req, res, ctx) {
65257
65396
  const msgMatch = /^\/api\/chat\/threads\/([^/]+)\/messages$/.exec(ctx.pathname);
65258
65397
  if (ctx.method === "GET" && msgMatch) {
65259
65398
  const threadId2 = decodeURIComponent(msgMatch[1] ?? "");
65260
- const msgFilters = [{ col: "thread_id", op: "eq", val: threadId2 }];
65399
+ if (cloud && ownerUserId == null) {
65400
+ sendJson3(res, { messages: [] });
65401
+ return true;
65402
+ }
65403
+ const msgFilters = [
65404
+ { col: "thread_id", op: "eq", val: threadId2 }
65405
+ ];
65406
+ if (ownerUserId != null) msgFilters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
65261
65407
  const rows = await ctx.db.query("chat_messages", {
65262
65408
  filters: msgFilters,
65263
65409
  limit: 1e3
65264
65410
  });
65265
- const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at).map((r6) => {
65411
+ const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at && ownedByMe(r6)).map((r6) => {
65266
65412
  let text = "";
65267
65413
  let turns2;
65268
65414
  let startedAt;
@@ -65315,12 +65461,21 @@ async function dispatchChatRoute(req, res, ctx) {
65315
65461
  return true;
65316
65462
  }
65317
65463
  const requestedThread = typeof body.threadId === "string" ? body.threadId : null;
65464
+ if (cloud && ownerUserId == null) {
65465
+ sendJson3(res, { error: "Could not resolve your cloud identity; chat is disabled." }, 500);
65466
+ return true;
65467
+ }
65318
65468
  const activeContext = parseActiveContext(body.activeContext, ctx.validTables);
65319
- const history = await rehydrateHistory(ctx.db, requestedThread, mapHistory(body.history), null);
65469
+ const history = await rehydrateHistory(
65470
+ ctx.db,
65471
+ requestedThread,
65472
+ mapHistory(body.history),
65473
+ ownerUserId
65474
+ );
65320
65475
  let threadId = "";
65321
65476
  try {
65322
- threadId = await ensureThread(ctx.db, requestedThread, message, null);
65323
- await persistMessage(ctx.db, threadId, "user", message, null);
65477
+ threadId = await ensureThread(ctx.db, requestedThread, message, ownerUserId);
65478
+ await persistMessage(ctx.db, threadId, "user", message, ownerUserId);
65324
65479
  } catch (e6) {
65325
65480
  console.warn("[chat] persist user message failed:", e6.message);
65326
65481
  }
@@ -65392,7 +65547,7 @@ async function dispatchChatRoute(req, res, ctx) {
65392
65547
  threadId,
65393
65548
  "assistant",
65394
65549
  assistantText,
65395
- null,
65550
+ ownerUserId,
65396
65551
  cleanTurns,
65397
65552
  turnStartedAt,
65398
65553
  assistantMsgId
@@ -66541,9 +66696,11 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
66541
66696
  if (await cloudRlsInstalled(db) && await canManageRoles(db)) {
66542
66697
  await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
66543
66698
  await installCloudRls(db);
66699
+ await ownPolyfillsByGroup(db);
66544
66700
  await installCloudSettings(db);
66545
66701
  await db.ensureObservationSubstrate();
66546
66702
  await enableChangelogRls(db);
66703
+ await enableChatPrivacyRls(db);
66547
66704
  const access = await reconcileCloudMemberAccess(db);
66548
66705
  convergeWarnings = access.skipped;
66549
66706
  for (const s2 of convergeWarnings) {
@@ -66705,11 +66862,22 @@ function startBackgroundRender(active) {
66705
66862
  active.eagerRenderWired = true;
66706
66863
  let lastFire = 0;
66707
66864
  let trailing;
66865
+ const pendingTables = /* @__PURE__ */ new Set();
66866
+ let pendingFull = false;
66708
66867
  const fire = () => {
66709
66868
  lastFire = Date.now();
66710
- active.db.requestRender();
66869
+ if (pendingFull || pendingTables.size === 0) {
66870
+ pendingFull = false;
66871
+ pendingTables.clear();
66872
+ active.db.requestRender();
66873
+ return;
66874
+ }
66875
+ for (const t8 of pendingTables) active.db.requestRender(t8);
66876
+ pendingTables.clear();
66711
66877
  };
66712
- active.realtime.subscribePayload(() => {
66878
+ active.realtime.subscribePayload((payload) => {
66879
+ if (payload.table_name) pendingTables.add(payload.table_name);
66880
+ else pendingFull = true;
66713
66881
  const since = Date.now() - lastFire;
66714
66882
  if (since >= EAGER_RERENDER_MIN_INTERVAL_MS) {
66715
66883
  fire();
@@ -69130,7 +69298,7 @@ function printHelp() {
69130
69298
  );
69131
69299
  }
69132
69300
  function getVersion() {
69133
- if (true) return "3.4.0";
69301
+ if (true) return "3.4.1";
69134
69302
  try {
69135
69303
  const pkgPath = new URL("../package.json", import.meta.url).pathname;
69136
69304
  const pkg = JSON.parse(readFileSync20(pkgPath, "utf-8"));