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/index.js CHANGED
@@ -908,43 +908,57 @@ var init_postgres = __esm({
908
908
  },
909
909
  {
910
910
  warn: "could not register json_extract polyfill:",
911
- sql: `CREATE OR REPLACE FUNCTION json_extract(doc text, path text)
912
- RETURNS text
913
- LANGUAGE sql
914
- IMMUTABLE
915
- AS $fn$
916
- SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
917
- $fn$;`
911
+ // Create ONLY if absent. `CREATE OR REPLACE` on an existing function requires
912
+ // ownership, but on a cloud the function is owned by whichever single role
913
+ // created it first — so every OTHER member's per-connect replace raised "must
914
+ // be owner of function" and (sharing the render transaction) aborted it,
915
+ // yielding an empty render. The IF-absent guard makes a present function a
916
+ // clean no-op for everyone, regardless of who owns it.
917
+ sql: `DO $do$ BEGIN
918
+ IF to_regprocedure('json_extract(text, text)') IS NULL THEN
919
+ CREATE FUNCTION json_extract(doc text, path text)
920
+ RETURNS text
921
+ LANGUAGE sql
922
+ IMMUTABLE
923
+ AS $fn$
924
+ SELECT doc::jsonb #>> string_to_array(regexp_replace(path, '^\\$\\.?', ''), '.')
925
+ $fn$;
926
+ END IF;
927
+ END $do$;`
918
928
  },
919
929
  {
920
930
  warn: "could not register strftime polyfill:",
921
- sql: `CREATE OR REPLACE FUNCTION strftime(format text, modifier text)
922
- RETURNS text
923
- LANGUAGE plpgsql
924
- IMMUTABLE
925
- AS $fn$
926
- DECLARE ts timestamptz;
927
- BEGIN
928
- IF modifier = 'now' THEN
929
- ts := now();
930
- ELSE
931
- ts := modifier::timestamptz;
932
- END IF;
933
- RETURN to_char(
934
- ts AT TIME ZONE 'UTC',
935
- replace(replace(replace(replace(replace(replace(replace(replace(
936
- format,
937
- '%Y', 'YYYY'),
938
- '%m', 'MM'),
939
- '%d', 'DD'),
940
- '%H', 'HH24'),
941
- '%M', 'MI'),
942
- '%S', 'SS'),
943
- '%f', 'MS'),
944
- 'T', '"T"')
945
- );
946
- END;
947
- $fn$;`
931
+ sql: `DO $do$ BEGIN
932
+ IF to_regprocedure('strftime(text, text)') IS NULL THEN
933
+ CREATE FUNCTION strftime(format text, modifier text)
934
+ RETURNS text
935
+ LANGUAGE plpgsql
936
+ IMMUTABLE
937
+ AS $fn$
938
+ DECLARE ts timestamptz;
939
+ BEGIN
940
+ IF modifier = 'now' THEN
941
+ ts := now();
942
+ ELSE
943
+ ts := modifier::timestamptz;
944
+ END IF;
945
+ RETURN to_char(
946
+ ts AT TIME ZONE 'UTC',
947
+ replace(replace(replace(replace(replace(replace(replace(replace(
948
+ format,
949
+ '%Y', 'YYYY'),
950
+ '%m', 'MM'),
951
+ '%d', 'DD'),
952
+ '%H', 'HH24'),
953
+ '%M', 'MI'),
954
+ '%S', 'SS'),
955
+ '%f', 'MS'),
956
+ 'T', '"T"')
957
+ );
958
+ END;
959
+ $fn$;
960
+ END IF;
961
+ END $do$;`
948
962
  }
949
963
  ];
950
964
  }
@@ -2352,6 +2366,34 @@ var init_engine = __esm({
2352
2366
  setRenderFold(fn) {
2353
2367
  this._foldRows = fn;
2354
2368
  }
2369
+ /**
2370
+ * Incremental scope: is this entity-context table affected by a change to one
2371
+ * of `changed`? Affected when the table itself changed (its own rows / `self`
2372
+ * source / index) OR any of its files SOURCES from a changed table (a cross-
2373
+ * table dependent — e.g. an AGENT.md that lists the agent's tasks must re-render
2374
+ * when `tasks` changes). A `custom` source runs an arbitrary query, so we can't
2375
+ * prove independence — treat it as always-affected (conservative, never stale).
2376
+ */
2377
+ _entityAffected(table, def, changed) {
2378
+ if (changed.has(table)) return true;
2379
+ for (const spec of Object.values(def.files)) {
2380
+ if (this._sourceTouches(spec.source, changed)) return true;
2381
+ }
2382
+ return false;
2383
+ }
2384
+ _sourceTouches(source, changed) {
2385
+ if (source == null || typeof source !== "object") return false;
2386
+ const s2 = source;
2387
+ if (s2.type === "custom") return true;
2388
+ if (typeof s2.table === "string" && changed.has(s2.table)) return true;
2389
+ if (typeof s2.junctionTable === "string" && changed.has(s2.junctionTable)) return true;
2390
+ if (s2.sources != null && typeof s2.sources === "object") {
2391
+ for (const sub of Object.values(s2.sources)) {
2392
+ if (this._sourceTouches(sub, changed)) return true;
2393
+ }
2394
+ }
2395
+ return false;
2396
+ }
2355
2397
  async render(outputDir, opts = {}) {
2356
2398
  const start = Date.now();
2357
2399
  const filesWritten = [];
@@ -2361,6 +2403,7 @@ var init_engine = __esm({
2361
2403
  for (const [name, def] of this._schema.getTables()) {
2362
2404
  if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
2363
2405
  if (this._skipEmpty && def.render === NOOP_RENDER) continue;
2406
+ if (opts.changedTables && !opts.changedTables.has(name)) continue;
2364
2407
  let rows = await this._schema.queryTable(this._adapter, name, this._readRel);
2365
2408
  if (def.relevanceFilter) {
2366
2409
  const ctx = this._getTaskContext();
@@ -2413,6 +2456,9 @@ var init_engine = __esm({
2413
2456
  }
2414
2457
  for (const [name, def] of this._schema.getMultis()) {
2415
2458
  if (signal?.aborted) return this._abortedResult(filesWritten, counters, start);
2459
+ if (opts.changedTables && def.tables && !def.tables.some((t8) => opts.changedTables?.has(t8))) {
2460
+ continue;
2461
+ }
2416
2462
  const keys = await def.keys();
2417
2463
  const tables = {};
2418
2464
  if (def.tables) {
@@ -2444,16 +2490,22 @@ var init_engine = __esm({
2444
2490
  filesWritten,
2445
2491
  counters,
2446
2492
  throttle,
2447
- signal
2493
+ signal,
2494
+ opts.changedTables
2448
2495
  );
2449
2496
  if (entityContextManifest === null) {
2450
2497
  return this._abortedResult(filesWritten, counters, start);
2451
2498
  }
2452
2499
  if (this._schema.getEntityContexts().size > 0) {
2500
+ let entityContexts = entityContextManifest;
2501
+ if (opts.changedTables) {
2502
+ const prev = readManifest(outputDir);
2503
+ entityContexts = { ...prev?.entityContexts ?? {}, ...entityContextManifest };
2504
+ }
2453
2505
  writeManifest(outputDir, {
2454
2506
  version: 2,
2455
2507
  generated_at: (/* @__PURE__ */ new Date()).toISOString(),
2456
- entityContexts: entityContextManifest
2508
+ entityContexts
2457
2509
  });
2458
2510
  }
2459
2511
  const result = {
@@ -2519,12 +2571,14 @@ var init_engine = __esm({
2519
2571
  * partial tree). Progress is reported through `throttle`; abort is observed
2520
2572
  * via `signal`.
2521
2573
  */
2522
- async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal) {
2574
+ async _renderEntityContexts(outputDir, filesWritten, counters, throttle, signal, changedTables) {
2523
2575
  const protectedTables = /* @__PURE__ */ new Set();
2524
2576
  for (const [t8, d6] of this._schema.getEntityContexts()) {
2525
2577
  if (d6.protected) protectedTables.add(t8);
2526
2578
  }
2527
- const entityTables = [...this._schema.getEntityContexts()];
2579
+ const entityTables = [...this._schema.getEntityContexts()].filter(
2580
+ ([table, def]) => !changedTables || this._entityAffected(table, def, changedTables)
2581
+ );
2528
2582
  const tableCount = entityTables.length;
2529
2583
  if (signal?.aborted) return null;
2530
2584
  const renderedEntries = await mapWithConcurrency(
@@ -5236,6 +5290,16 @@ var init_lattice = __esm({
5236
5290
  _autoRenderPending = false;
5237
5291
  _autoRenderInFlight = false;
5238
5292
  _autoRenderDebounceMs = 250;
5293
+ /**
5294
+ * Incremental auto-render scope, accumulated between debounced renders. A write
5295
+ * or a remote (cloud) change records the AFFECTED table here, so the next
5296
+ * auto-render re-renders only that entity (+ its cross-table dependents) instead
5297
+ * of the whole tree. `_pendingRenderAll` forces a full render (the initial
5298
+ * render, or a change with no known table). Captured + reset when a render
5299
+ * starts, so changes during a render re-accumulate and re-trigger.
5300
+ */
5301
+ _pendingRenderTables = /* @__PURE__ */ new Set();
5302
+ _pendingRenderAll = true;
5239
5303
  /** Cache of actual table columns (from PRAGMA), populated after init(). */
5240
5304
  _columnCache = /* @__PURE__ */ new Map();
5241
5305
  /** Derived encryption key (from options.encryptionKey via scrypt). */
@@ -6331,7 +6395,7 @@ var init_lattice = __esm({
6331
6395
  `${verb} INTO "${junctionTable}" (${colNames}) VALUES (${placeholders})`,
6332
6396
  Object.values(filtered)
6333
6397
  );
6334
- this._scheduleAutoRender();
6398
+ this._scheduleAutoRender(junctionTable);
6335
6399
  }
6336
6400
  /**
6337
6401
  * Delete rows from a junction table matching all given conditions.
@@ -6347,7 +6411,7 @@ var init_lattice = __esm({
6347
6411
  `DELETE FROM "${junctionTable}" WHERE ${where}`,
6348
6412
  entries.map(([, v2]) => v2)
6349
6413
  );
6350
- this._scheduleAutoRender();
6414
+ this._scheduleAutoRender(junctionTable);
6351
6415
  }
6352
6416
  // -------------------------------------------------------------------------
6353
6417
  // Seeding DSL (v0.13+)
@@ -6603,6 +6667,11 @@ var init_lattice = __esm({
6603
6667
  async renderInBackground(outputDir, opts = {}) {
6604
6668
  const notInit = this._notInitError();
6605
6669
  if (notInit) return notInit;
6670
+ if (!opts.changedTables) {
6671
+ this._pendingRenderAll = false;
6672
+ this._pendingRenderTables = /* @__PURE__ */ new Set();
6673
+ this._autoRenderPending = false;
6674
+ }
6606
6675
  return this._renderGuarded(outputDir, opts);
6607
6676
  }
6608
6677
  /**
@@ -6632,9 +6701,12 @@ var init_lattice = __esm({
6632
6701
  * tree when a REMOTE change arrives — notably an owner re-sharing or un-sharing
6633
6702
  * a row, after which the member's per-viewer projection must be recompiled. A
6634
6703
  * no-op when auto-render isn't enabled.
6704
+ *
6705
+ * Pass the CHANGED table so only that entity (+ its cross-table dependents) is
6706
+ * re-rendered instead of the whole tree; omit it to force a full render.
6635
6707
  */
6636
- requestRender() {
6637
- this._scheduleAutoRender();
6708
+ requestRender(table) {
6709
+ this._scheduleAutoRender(table);
6638
6710
  }
6639
6711
  /**
6640
6712
  * True while a render is actively writing the context tree + manifest (auto-
@@ -7035,7 +7107,7 @@ var init_lattice = __esm({
7035
7107
  for (const h6 of this._errorHandlers) h6(err instanceof Error ? err : new Error(String(err)));
7036
7108
  }
7037
7109
  }
7038
- this._scheduleAutoRender();
7110
+ this._scheduleAutoRender(table);
7039
7111
  }
7040
7112
  /**
7041
7113
  * Turn on automatic rendering into `outputDir`. After this, every insert /
@@ -7059,10 +7131,18 @@ var init_lattice = __esm({
7059
7131
  this._autoRenderPending = false;
7060
7132
  return this;
7061
7133
  }
7062
- _scheduleAutoRender() {
7134
+ _scheduleAutoRender(table) {
7063
7135
  if (!this._autoRenderDir) return;
7136
+ if (table === void 0) this._pendingRenderAll = true;
7137
+ else this._pendingRenderTables.add(table);
7064
7138
  this._autoRenderPending = true;
7065
- if (this._autoRenderTimer) return;
7139
+ this._armAutoRenderTimer();
7140
+ }
7141
+ /** Arm the debounce timer if not already armed. Does NOT change the render
7142
+ * scope — used both by `_scheduleAutoRender` and the post-render re-arm so a
7143
+ * re-arm never escalates a pending incremental render to a full one. */
7144
+ _armAutoRenderTimer() {
7145
+ if (!this._autoRenderDir || this._autoRenderTimer) return;
7066
7146
  this._autoRenderTimer = setTimeout(() => {
7067
7147
  this._autoRenderTimer = void 0;
7068
7148
  void this._runAutoRender();
@@ -7102,10 +7182,15 @@ var init_lattice = __esm({
7102
7182
  }
7103
7183
  if (!this._autoRenderPending) return;
7104
7184
  this._autoRenderPending = false;
7185
+ const renderAll = this._pendingRenderAll;
7186
+ const changed = this._pendingRenderTables;
7187
+ this._pendingRenderAll = false;
7188
+ this._pendingRenderTables = /* @__PURE__ */ new Set();
7105
7189
  this._autoRenderInFlight = true;
7106
7190
  try {
7107
7191
  const prevManifest = readManifest(dir);
7108
- const result = await this._render.render(dir);
7192
+ const renderOpts = renderAll || changed.size === 0 ? {} : { changedTables: changed };
7193
+ const result = await this._render.render(dir, renderOpts);
7109
7194
  for (const h6 of this._renderHandlers) h6(result);
7110
7195
  const newManifest = readManifest(dir);
7111
7196
  await this._render.cleanup(dir, prevManifest, {}, newManifest);
@@ -7117,7 +7202,7 @@ var init_lattice = __esm({
7117
7202
  }
7118
7203
  }
7119
7204
  _rearmAutoRenderIfPending() {
7120
- if (this._autoRenderPending) this._scheduleAutoRender();
7205
+ if (this._autoRenderPending) this._armAutoRenderTimer();
7121
7206
  }
7122
7207
  /**
7123
7208
  * Update or remove the embedding for a row.
@@ -47275,6 +47360,17 @@ CREATE TRIGGER "${trg}" AFTER INSERT OR UPDATE OR DELETE ON ${q3}
47275
47360
  FOR EACH ROW EXECUTE FUNCTION "${trg}"();
47276
47361
  `;
47277
47362
  }
47363
+ async function ownPolyfillsByGroup(db) {
47364
+ if (!isPg(db)) return;
47365
+ for (const sig of ["json_extract(text, text)", "strftime(text, text)"]) {
47366
+ try {
47367
+ const reg = await getAsyncOrSync(db.adapter, `SELECT to_regprocedure($1) AS reg`, [sig]);
47368
+ if (reg?.reg == null) continue;
47369
+ await runAsyncOrSync(db.adapter, `ALTER FUNCTION ${sig} OWNER TO "${MEMBER_GROUP}"`);
47370
+ } catch {
47371
+ }
47372
+ }
47373
+ }
47278
47374
  async function installCloudRls(db) {
47279
47375
  if (!isPg(db)) return;
47280
47376
  const schema = await cloudSchema(db);
@@ -47308,6 +47404,24 @@ CREATE POLICY "lattice_changelog_ins" ON "__lattice_changelog" FOR INSERT WITH C
47308
47404
  `
47309
47405
  );
47310
47406
  }
47407
+ async function enableChatPrivacyRls(db) {
47408
+ if (!isPg(db)) return;
47409
+ for (const t8 of ["chat_threads", "chat_messages"]) {
47410
+ const reg = await getAsyncOrSync(db.adapter, `SELECT to_regclass($1) AS reg`, [t8]);
47411
+ if (reg?.reg == null) continue;
47412
+ const q3 = `"${t8}"`;
47413
+ await runCloudBootstrapSql(
47414
+ db,
47415
+ `
47416
+ ALTER TABLE ${q3} ENABLE ROW LEVEL SECURITY;
47417
+ ALTER TABLE ${q3} FORCE ROW LEVEL SECURITY;
47418
+ DROP POLICY IF EXISTS "lattice_chat_owner" ON ${q3};
47419
+ CREATE POLICY "lattice_chat_owner" ON ${q3} AS RESTRICTIVE FOR SELECT
47420
+ USING ("owner_user_id" IS NOT NULL AND "owner_user_id" = session_user);
47421
+ `
47422
+ );
47423
+ }
47424
+ }
47311
47425
  async function enableRlsForTable(db, table, pkCols) {
47312
47426
  if (!isPg(db)) return;
47313
47427
  const schema = await cloudSchema(db);
@@ -54498,9 +54612,11 @@ async function secureCloud(db) {
54498
54612
  if (db.getDialect() !== "postgres") return;
54499
54613
  await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
54500
54614
  await installCloudRls(db);
54615
+ await ownPolyfillsByGroup(db);
54501
54616
  await installCloudSettings(db);
54502
54617
  await db.ensureObservationSubstrate();
54503
54618
  await enableChangelogRls(db);
54619
+ await enableChatPrivacyRls(db);
54504
54620
  await convergeLegacyColumnAudience(db);
54505
54621
  const registered = db.getRegisteredTableNames();
54506
54622
  for (const table of registered) {
@@ -57553,12 +57669,17 @@ var appJs = `
57553
57669
  fetchJson('/api/gui-meta/columns').catch(function () { return {}; }),
57554
57670
  fetchJson('/api/system-tables').catch(function () { return { tables: [] }; }),
57555
57671
  fetchJson('/api/workspaces').catch(function () { return null; }),
57672
+ fetchJson('/api/dbconfig').catch(function () { return {}; }),
57556
57673
  ]).then(function (results) {
57557
57674
  state.entities = results[0];
57558
57675
  state.iconOverrides = results[1] || {};
57559
57676
  state.columnMeta = results[2] || {};
57560
57677
  state.systemTables = (results[3] && results[3].tables) || [];
57561
57678
  renderWsSwitcher(results[4]);
57679
+ // Re-point the header logo at the NEW workspace's mark \u2014 the switch path
57680
+ // must refresh branding the way boot does (the etag cache-busts the
57681
+ // <img>), else the previous workspace's logo stays until a hard refresh.
57682
+ applyWorkspaceLogo((results[5] || {}).logoEtag);
57562
57683
  renderSidebar();
57563
57684
  // renderWsSwitcher set cloudMode from the new workspace's kind; re-render
57564
57685
  // the composer so the Private-mode toggle reflects local vs cloud (it is
@@ -66208,6 +66329,7 @@ function activeCloudCoords(configPath) {
66208
66329
  init_assistant_routes();
66209
66330
 
66210
66331
  // src/gui/chat-routes.ts
66332
+ init_adapter();
66211
66333
  init_assistant_routes();
66212
66334
  init_chat();
66213
66335
  init_user_config();
@@ -66233,6 +66355,15 @@ function sendJson3(res, body, status = 200) {
66233
66355
  function asStr(v2, fallback = "") {
66234
66356
  return typeof v2 === "string" ? v2 : fallback;
66235
66357
  }
66358
+ function isCloudChat(db) {
66359
+ return db.getDialect() === "postgres";
66360
+ }
66361
+ async function resolveChatOwnerId(db) {
66362
+ if (!isCloudChat(db)) return null;
66363
+ const row = await getAsyncOrSync(db.adapter, "SELECT session_user AS u");
66364
+ const u2 = row?.u;
66365
+ return typeof u2 === "string" && u2.length > 0 ? u2 : null;
66366
+ }
66236
66367
  function readJson3(req) {
66237
66368
  return new Promise((resolve12, reject) => {
66238
66369
  let raw = "";
@@ -66425,12 +66556,20 @@ async function persistMessage(db, threadId, role, text, ownerUserId, turns, star
66425
66556
  });
66426
66557
  }
66427
66558
  async function dispatchChatRoute(req, res, ctx) {
66559
+ const ownerUserId = await resolveChatOwnerId(ctx.db);
66560
+ const cloud = isCloudChat(ctx.db);
66561
+ const ownedByMe = (r6) => !cloud || r6.owner_user_id != null && r6.owner_user_id === ownerUserId;
66428
66562
  if (ctx.method === "GET" && ctx.pathname === "/api/chat/threads") {
66563
+ if (cloud && ownerUserId == null) {
66564
+ sendJson3(res, { threads: [] });
66565
+ return true;
66566
+ }
66429
66567
  const filters = [
66430
66568
  { col: "deleted_at", op: "isNull" }
66431
66569
  ];
66570
+ if (ownerUserId != null) filters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
66432
66571
  const rows = await ctx.db.query("chat_threads", { filters, limit: 100 });
66433
- const threads = rows.filter((r6) => !r6.deleted_at).map((r6) => ({
66572
+ const threads = rows.filter((r6) => !r6.deleted_at && ownedByMe(r6)).map((r6) => ({
66434
66573
  id: asStr(r6.id),
66435
66574
  title: asStr(r6.title, "Chat"),
66436
66575
  created_at: asStr(r6.created_at)
@@ -66441,12 +66580,19 @@ async function dispatchChatRoute(req, res, ctx) {
66441
66580
  const msgMatch = /^\/api\/chat\/threads\/([^/]+)\/messages$/.exec(ctx.pathname);
66442
66581
  if (ctx.method === "GET" && msgMatch) {
66443
66582
  const threadId2 = decodeURIComponent(msgMatch[1] ?? "");
66444
- const msgFilters = [{ col: "thread_id", op: "eq", val: threadId2 }];
66583
+ if (cloud && ownerUserId == null) {
66584
+ sendJson3(res, { messages: [] });
66585
+ return true;
66586
+ }
66587
+ const msgFilters = [
66588
+ { col: "thread_id", op: "eq", val: threadId2 }
66589
+ ];
66590
+ if (ownerUserId != null) msgFilters.push({ col: "owner_user_id", op: "eq", val: ownerUserId });
66445
66591
  const rows = await ctx.db.query("chat_messages", {
66446
66592
  filters: msgFilters,
66447
66593
  limit: 1e3
66448
66594
  });
66449
- const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at).map((r6) => {
66595
+ const messages = rows.filter((r6) => r6.thread_id === threadId2 && !r6.deleted_at && ownedByMe(r6)).map((r6) => {
66450
66596
  let text = "";
66451
66597
  let turns2;
66452
66598
  let startedAt;
@@ -66499,12 +66645,21 @@ async function dispatchChatRoute(req, res, ctx) {
66499
66645
  return true;
66500
66646
  }
66501
66647
  const requestedThread = typeof body.threadId === "string" ? body.threadId : null;
66648
+ if (cloud && ownerUserId == null) {
66649
+ sendJson3(res, { error: "Could not resolve your cloud identity; chat is disabled." }, 500);
66650
+ return true;
66651
+ }
66502
66652
  const activeContext = parseActiveContext(body.activeContext, ctx.validTables);
66503
- const history = await rehydrateHistory(ctx.db, requestedThread, mapHistory(body.history), null);
66653
+ const history = await rehydrateHistory(
66654
+ ctx.db,
66655
+ requestedThread,
66656
+ mapHistory(body.history),
66657
+ ownerUserId
66658
+ );
66504
66659
  let threadId = "";
66505
66660
  try {
66506
- threadId = await ensureThread(ctx.db, requestedThread, message, null);
66507
- await persistMessage(ctx.db, threadId, "user", message, null);
66661
+ threadId = await ensureThread(ctx.db, requestedThread, message, ownerUserId);
66662
+ await persistMessage(ctx.db, threadId, "user", message, ownerUserId);
66508
66663
  } catch (e6) {
66509
66664
  console.warn("[chat] persist user message failed:", e6.message);
66510
66665
  }
@@ -66576,7 +66731,7 @@ async function dispatchChatRoute(req, res, ctx) {
66576
66731
  threadId,
66577
66732
  "assistant",
66578
66733
  assistantText,
66579
- null,
66734
+ ownerUserId,
66580
66735
  cleanTurns,
66581
66736
  turnStartedAt,
66582
66737
  assistantMsgId
@@ -67575,9 +67730,11 @@ async function openConfig(configPath, outputDir, autoRender = false, realtimeWat
67575
67730
  if (await cloudRlsInstalled(db) && await canManageRoles(db)) {
67576
67731
  await registerPostgresPolyfills((sql) => runAsyncOrSync(db.adapter, sql));
67577
67732
  await installCloudRls(db);
67733
+ await ownPolyfillsByGroup(db);
67578
67734
  await installCloudSettings(db);
67579
67735
  await db.ensureObservationSubstrate();
67580
67736
  await enableChangelogRls(db);
67737
+ await enableChatPrivacyRls(db);
67581
67738
  const access = await reconcileCloudMemberAccess(db);
67582
67739
  convergeWarnings = access.skipped;
67583
67740
  for (const s2 of convergeWarnings) {
@@ -67739,11 +67896,22 @@ function startBackgroundRender(active) {
67739
67896
  active.eagerRenderWired = true;
67740
67897
  let lastFire = 0;
67741
67898
  let trailing;
67899
+ const pendingTables = /* @__PURE__ */ new Set();
67900
+ let pendingFull = false;
67742
67901
  const fire = () => {
67743
67902
  lastFire = Date.now();
67744
- active.db.requestRender();
67903
+ if (pendingFull || pendingTables.size === 0) {
67904
+ pendingFull = false;
67905
+ pendingTables.clear();
67906
+ active.db.requestRender();
67907
+ return;
67908
+ }
67909
+ for (const t8 of pendingTables) active.db.requestRender(t8);
67910
+ pendingTables.clear();
67745
67911
  };
67746
- active.realtime.subscribePayload(() => {
67912
+ active.realtime.subscribePayload((payload) => {
67913
+ if (payload.table_name) pendingTables.add(payload.table_name);
67914
+ else pendingFull = true;
67747
67915
  const since = Date.now() - lastFire;
67748
67916
  if (since >= EAGER_RERENDER_MIN_INTERVAL_MS) {
67749
67917
  fire();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latticesql",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "Persistent structured memory for AI agent systems — pluggable SQLite or Postgres backend, LLM context bridge",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",