@deeplake/hivemind 0.7.34 → 0.7.36

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.
@@ -6,13 +6,13 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Cloud-backed persistent shared memory for AI agents powered by Deeplake",
9
- "version": "0.7.34"
9
+ "version": "0.7.36"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "hivemind",
14
14
  "description": "Persistent shared memory powered by Deeplake — captures all session activity and provides cross-session, cross-agent memory search",
15
- "version": "0.7.34",
15
+ "version": "0.7.36",
16
16
  "source": "./claude-code",
17
17
  "homepage": "https://github.com/activeloopai/hivemind"
18
18
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hivemind",
3
3
  "description": "Cloud-backed persistent memory powered by Deeplake — read, write, and share memory across Claude Code sessions and agents",
4
- "version": "0.7.34",
4
+ "version": "0.7.36",
5
5
  "author": {
6
6
  "name": "Activeloop",
7
7
  "url": "https://deeplake.ai"
package/bundle/cli.js CHANGED
@@ -4341,7 +4341,123 @@ function sqlIdent(name) {
4341
4341
 
4342
4342
  // dist/src/embeddings/columns.js
4343
4343
  var SUMMARY_EMBEDDING_COL = "summary_embedding";
4344
- var MESSAGE_EMBEDDING_COL = "message_embedding";
4344
+
4345
+ // dist/src/deeplake-schema.js
4346
+ var MEMORY_COLUMNS = Object.freeze([
4347
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
4348
+ { name: "path", sql: "TEXT NOT NULL DEFAULT ''" },
4349
+ { name: "filename", sql: "TEXT NOT NULL DEFAULT ''" },
4350
+ { name: "summary", sql: "TEXT NOT NULL DEFAULT ''" },
4351
+ { name: "summary_embedding", sql: "FLOAT4[]" },
4352
+ { name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
4353
+ { name: "mime_type", sql: "TEXT NOT NULL DEFAULT 'text/plain'" },
4354
+ { name: "size_bytes", sql: "BIGINT NOT NULL DEFAULT 0" },
4355
+ { name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
4356
+ { name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
4357
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT ''" },
4358
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" },
4359
+ { name: "creation_date", sql: "TEXT NOT NULL DEFAULT ''" },
4360
+ { name: "last_update_date", sql: "TEXT NOT NULL DEFAULT ''" }
4361
+ ]);
4362
+ var SESSIONS_COLUMNS = Object.freeze([
4363
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
4364
+ { name: "path", sql: "TEXT NOT NULL DEFAULT ''" },
4365
+ { name: "filename", sql: "TEXT NOT NULL DEFAULT ''" },
4366
+ { name: "message", sql: "JSONB" },
4367
+ { name: "message_embedding", sql: "FLOAT4[]" },
4368
+ { name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
4369
+ { name: "mime_type", sql: "TEXT NOT NULL DEFAULT 'application/json'" },
4370
+ { name: "size_bytes", sql: "BIGINT NOT NULL DEFAULT 0" },
4371
+ { name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
4372
+ { name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
4373
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT ''" },
4374
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" },
4375
+ { name: "creation_date", sql: "TEXT NOT NULL DEFAULT ''" },
4376
+ { name: "last_update_date", sql: "TEXT NOT NULL DEFAULT ''" }
4377
+ ]);
4378
+ var SKILLS_COLUMNS = Object.freeze([
4379
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
4380
+ { name: "name", sql: "TEXT NOT NULL DEFAULT ''" },
4381
+ { name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
4382
+ { name: "project_key", sql: "TEXT NOT NULL DEFAULT ''" },
4383
+ { name: "local_path", sql: "TEXT NOT NULL DEFAULT ''" },
4384
+ { name: "install", sql: "TEXT NOT NULL DEFAULT 'project'" },
4385
+ { name: "source_sessions", sql: "TEXT NOT NULL DEFAULT '[]'" },
4386
+ { name: "source_agent", sql: "TEXT NOT NULL DEFAULT ''" },
4387
+ { name: "scope", sql: "TEXT NOT NULL DEFAULT 'me'" },
4388
+ { name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
4389
+ { name: "contributors", sql: "TEXT NOT NULL DEFAULT '[]'" },
4390
+ { name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
4391
+ { name: "trigger_text", sql: "TEXT NOT NULL DEFAULT ''" },
4392
+ { name: "body", sql: "TEXT NOT NULL DEFAULT ''" },
4393
+ { name: "version", sql: "BIGINT NOT NULL DEFAULT 1" },
4394
+ { name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
4395
+ { name: "updated_at", sql: "TEXT NOT NULL DEFAULT ''" }
4396
+ ]);
4397
+ function validateSchema(label, cols) {
4398
+ const seen = /* @__PURE__ */ new Set();
4399
+ for (const col of cols) {
4400
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(col.name)) {
4401
+ throw new Error(`${label}: column name "${col.name}" is not a valid SQL identifier`);
4402
+ }
4403
+ if (seen.has(col.name)) {
4404
+ throw new Error(`${label}: duplicate column "${col.name}"`);
4405
+ }
4406
+ seen.add(col.name);
4407
+ const notNull = /\bNOT\s+NULL\b/i.test(col.sql);
4408
+ const hasDefault = /\bDEFAULT\b/i.test(col.sql);
4409
+ if (notNull && !hasDefault) {
4410
+ throw new Error(`${label}: column "${col.name}" is NOT NULL but has no DEFAULT \u2014 ALTER TABLE ADD COLUMN on a populated table would fail.`);
4411
+ }
4412
+ }
4413
+ }
4414
+ validateSchema("MEMORY_COLUMNS", MEMORY_COLUMNS);
4415
+ validateSchema("SESSIONS_COLUMNS", SESSIONS_COLUMNS);
4416
+ validateSchema("SKILLS_COLUMNS", SKILLS_COLUMNS);
4417
+ function buildCreateTableSql(tableName, cols) {
4418
+ const safe = sqlIdent(tableName);
4419
+ const colSql = cols.map((c) => `${c.name} ${c.sql}`).join(", ");
4420
+ return `CREATE TABLE IF NOT EXISTS "${safe}" (${colSql}) USING deeplake`;
4421
+ }
4422
+ function buildIntrospectionSql(tableName, workspaceId) {
4423
+ return `SELECT column_name FROM information_schema.columns WHERE table_name = '${sqlStr(tableName)}' AND table_schema = '${sqlStr(workspaceId)}'`;
4424
+ }
4425
+ async function healMissingColumns(args) {
4426
+ const safeTable = sqlIdent(args.tableName);
4427
+ const introspectSql = buildIntrospectionSql(args.tableName, args.workspaceId);
4428
+ const rows = await args.query(introspectSql);
4429
+ const existing = /* @__PURE__ */ new Set();
4430
+ for (const row of rows) {
4431
+ const v = row?.column_name;
4432
+ if (typeof v === "string")
4433
+ existing.add(v.toLowerCase());
4434
+ }
4435
+ const missingCols = args.columns.filter((c) => !existing.has(c.name.toLowerCase()));
4436
+ const missing = missingCols.map((c) => c.name);
4437
+ if (missingCols.length === 0)
4438
+ return { missing, altered: [] };
4439
+ const altered = [];
4440
+ for (const col of missingCols) {
4441
+ try {
4442
+ await args.query(`ALTER TABLE "${safeTable}" ADD COLUMN ${col.name} ${col.sql}`);
4443
+ altered.push(col.name);
4444
+ args.log?.(`schema-heal: added "${args.tableName}"."${col.name}"`);
4445
+ } catch (e) {
4446
+ const msg = e instanceof Error ? e.message : String(e);
4447
+ if (!/already exists/i.test(msg))
4448
+ throw e;
4449
+ const recheck = await args.query(introspectSql);
4450
+ const present = recheck.some((r) => {
4451
+ const v = r?.column_name;
4452
+ return typeof v === "string" && v.toLowerCase() === col.name.toLowerCase();
4453
+ });
4454
+ if (!present)
4455
+ throw e;
4456
+ args.log?.(`schema-heal: "${args.tableName}"."${col.name}" appeared via race, treating as success`);
4457
+ }
4458
+ }
4459
+ return { missing, altered };
4460
+ }
4345
4461
 
4346
4462
  // dist/src/notifications/queue.js
4347
4463
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, renameSync as renameSync3, mkdirSync as mkdirSync4, openSync, closeSync, unlinkSync as unlinkSync7, statSync as statSync2 } from "node:fs";
@@ -4707,64 +4823,33 @@ var DeeplakeApi = class {
4707
4823
  }
4708
4824
  }
4709
4825
  /**
4710
- * Ensure a vector column exists on the given table.
4711
- *
4712
- * The previous implementation always issued `ALTER TABLE ADD COLUMN IF NOT
4713
- * EXISTS …` on every SessionStart. On a long-running workspace that's
4714
- * already migrated, every call returns 500 "Column already exists" — noisy
4715
- * in the log and a wasted round-trip. Worse, the very first call after the
4716
- * column is genuinely added triggers Deeplake's post-ALTER `vector::at`
4717
- * window (~30s) during which subsequent INSERTs fail; minimising the
4718
- * number of ALTER calls minimises exposure to that window.
4826
+ * Heal any missing columns on a table so it matches one of the schema
4827
+ * definitions in `deeplake-schema.ts`. One SELECT against
4828
+ * `information_schema.columns` per call, then `ALTER TABLE ADD COLUMN`
4829
+ * only the genuinely missing ones never blanket, never `IF NOT
4830
+ * EXISTS`.
4719
4831
  *
4720
- * New flow:
4721
- * 1. Check the local marker file (mirrors ensureLookupIndex). If fresh,
4722
- * return zero network calls.
4723
- * 2. SELECT 1 FROM information_schema.columns WHERE table_name = T AND
4724
- * column_name = C. Read-only, idempotent, can't tickle the post-ALTER
4725
- * bug. If the column is present mark + return.
4726
- * 3. Only if step 2 says the column is missing, fall back to ALTER ADD
4727
- * COLUMN IF NOT EXISTS. Mark on success, also mark if Deeplake reports
4728
- * "already exists" (race: another client added it between our SELECT
4729
- * and ALTER).
4730
- *
4731
- * Marker uses the same dir / TTL as ensureLookupIndex so both schema
4732
- * caches share an opt-out (HIVEMIND_INDEX_MARKER_DIR) and a TTL knob.
4832
+ * History: an earlier path used a local marker file (`col_<name>` under
4833
+ * the index-marker dir) to skip even the SELECT after the first
4834
+ * confirmation, plus per-column ALTERs for `summary_embedding`,
4835
+ * `message_embedding`, `agent`, `plugin_version`. The marker existed
4836
+ * because Deeplake used to expose a ~30s post-ALTER bug where
4837
+ * subsequent INSERTs failed, so we wanted to keep ALTER traffic to a
4838
+ * minimum. The bug was re-verified on 2026-05-18 against
4839
+ * `api.deeplake.ai` (`test_plugin` org) and no longer reproduces
4840
+ * (71/71 INSERTs OK, first success 2ms after ALTER). The single SELECT
4841
+ * + targeted ALTER pattern survives the marker removal because: each
4842
+ * ALTER still costs ~800ms (so blanket sweeps are wasteful) and the
4843
+ * diff produces clearer logs than "ALTER all with IF NOT EXISTS".
4733
4844
  */
4734
- async ensureEmbeddingColumn(table, column) {
4735
- await this.ensureColumn(table, column, "FLOAT4[]");
4736
- }
4737
- /**
4738
- * Generic marker-gated column migration. Same SELECT-then-ALTER flow as
4739
- * ensureEmbeddingColumn, parameterized by SQL type so it can patch up any
4740
- * column that was added to the schema after the table was originally
4741
- * created. Used today for `summary_embedding`, `message_embedding`, and
4742
- * the `agent` column (added 2026-04-11) — the latter has no fallback if
4743
- * a user upgraded over a pre-2026-04-11 table, so every INSERT fails
4744
- * with `column "agent" does not exist`.
4745
- */
4746
- async ensureColumn(table, column, sqlType) {
4747
- const markers = await getIndexMarkerStore();
4748
- const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, `col_${column}`);
4749
- if (markers.hasFreshIndexMarker(markerPath))
4750
- return;
4751
- const colCheck = `SELECT 1 FROM information_schema.columns WHERE table_name = '${sqlStr(table)}' AND column_name = '${sqlStr(column)}' AND table_schema = '${sqlStr(this.workspaceId)}' LIMIT 1`;
4752
- const rows = await this.query(colCheck);
4753
- if (rows.length > 0) {
4754
- markers.writeIndexMarker(markerPath);
4755
- return;
4756
- }
4757
- try {
4758
- await this.query(`ALTER TABLE "${table}" ADD COLUMN ${column} ${sqlType}`);
4759
- } catch (e) {
4760
- const msg = e instanceof Error ? e.message : String(e);
4761
- if (!/already exists/i.test(msg))
4762
- throw e;
4763
- const recheck = await this.query(colCheck);
4764
- if (recheck.length === 0)
4765
- throw e;
4766
- }
4767
- markers.writeIndexMarker(markerPath);
4845
+ async healSchema(table, columns) {
4846
+ await healMissingColumns({
4847
+ query: (sql) => this.query(sql),
4848
+ tableName: table,
4849
+ workspaceId: this.workspaceId,
4850
+ columns,
4851
+ log: log4
4852
+ });
4768
4853
  }
4769
4854
  /** List all tables in the workspace (with retry). */
4770
4855
  async listTables(forceRefresh = false) {
@@ -4835,20 +4920,21 @@ var DeeplakeApi = class {
4835
4920
  }
4836
4921
  throw lastErr;
4837
4922
  }
4838
- /** Create the memory table if it doesn't already exist. Migrate columns on existing tables. */
4923
+ /** Create the memory table if it doesn't already exist. Heal missing columns on existing tables. */
4839
4924
  async ensureTable(name) {
4925
+ if (!MEMORY_COLUMNS.some((c) => c.name === SUMMARY_EMBEDDING_COL)) {
4926
+ throw new Error(`MEMORY_COLUMNS missing "${SUMMARY_EMBEDDING_COL}" (embeddings/columns.ts drift)`);
4927
+ }
4840
4928
  const tbl = sqlIdent(name ?? this.tableName);
4841
4929
  const tables = await this.listTables();
4842
4930
  if (!tables.includes(tbl)) {
4843
4931
  log4(`table "${tbl}" not found, creating`);
4844
- await this.createTableWithRetry(`CREATE TABLE IF NOT EXISTS "${tbl}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', summary TEXT NOT NULL DEFAULT '', summary_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'text/plain', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', plugin_version TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`, tbl);
4932
+ await this.createTableWithRetry(buildCreateTableSql(tbl, MEMORY_COLUMNS), tbl);
4845
4933
  log4(`table "${tbl}" created`);
4846
4934
  if (!tables.includes(tbl))
4847
4935
  this._tablesCache = [...tables, tbl];
4848
4936
  }
4849
- await this.ensureEmbeddingColumn(tbl, SUMMARY_EMBEDDING_COL);
4850
- await this.ensureColumn(tbl, "agent", "TEXT NOT NULL DEFAULT ''");
4851
- await this.ensureColumn(tbl, "plugin_version", "TEXT NOT NULL DEFAULT ''");
4937
+ await this.healSchema(tbl, MEMORY_COLUMNS);
4852
4938
  }
4853
4939
  /** Create the sessions table (uses JSONB for message since every row is a JSON event). */
4854
4940
  async ensureSessionsTable(name) {
@@ -4856,14 +4942,12 @@ var DeeplakeApi = class {
4856
4942
  const tables = await this.listTables();
4857
4943
  if (!tables.includes(safe)) {
4858
4944
  log4(`table "${safe}" not found, creating`);
4859
- await this.createTableWithRetry(`CREATE TABLE IF NOT EXISTS "${safe}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', message JSONB, message_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'application/json', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', plugin_version TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`, safe);
4945
+ await this.createTableWithRetry(buildCreateTableSql(safe, SESSIONS_COLUMNS), safe);
4860
4946
  log4(`table "${safe}" created`);
4861
4947
  if (!tables.includes(safe))
4862
4948
  this._tablesCache = [...tables, safe];
4863
4949
  }
4864
- await this.ensureEmbeddingColumn(safe, MESSAGE_EMBEDDING_COL);
4865
- await this.ensureColumn(safe, "agent", "TEXT NOT NULL DEFAULT ''");
4866
- await this.ensureColumn(safe, "plugin_version", "TEXT NOT NULL DEFAULT ''");
4950
+ await this.healSchema(safe, SESSIONS_COLUMNS);
4867
4951
  await this.ensureLookupIndex(safe, "path_creation_date", `("path", "creation_date")`);
4868
4952
  }
4869
4953
  /**
@@ -4881,11 +4965,12 @@ var DeeplakeApi = class {
4881
4965
  const tables = await this.listTables();
4882
4966
  if (!tables.includes(safe)) {
4883
4967
  log4(`table "${safe}" not found, creating`);
4884
- await this.createTableWithRetry(`CREATE TABLE IF NOT EXISTS "${safe}" (id TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', project TEXT NOT NULL DEFAULT '', project_key TEXT NOT NULL DEFAULT '', local_path TEXT NOT NULL DEFAULT '', install TEXT NOT NULL DEFAULT 'project', source_sessions TEXT NOT NULL DEFAULT '[]', source_agent TEXT NOT NULL DEFAULT '', scope TEXT NOT NULL DEFAULT 'me', author TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', trigger_text TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', version BIGINT NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL DEFAULT '') USING deeplake`, safe);
4968
+ await this.createTableWithRetry(buildCreateTableSql(safe, SKILLS_COLUMNS), safe);
4885
4969
  log4(`table "${safe}" created`);
4886
4970
  if (!tables.includes(safe))
4887
4971
  this._tablesCache = [...tables, safe];
4888
4972
  }
4973
+ await this.healSchema(safe, SKILLS_COLUMNS);
4889
4974
  await this.ensureLookupIndex(safe, "project_key_name", `("project_key", "name")`);
4890
4975
  }
4891
4976
  };
@@ -136,7 +136,6 @@ function sqlIdent(name) {
136
136
 
137
137
  // dist/src/embeddings/columns.js
138
138
  var SUMMARY_EMBEDDING_COL = "summary_embedding";
139
- var MESSAGE_EMBEDDING_COL = "message_embedding";
140
139
 
141
140
  // dist/src/utils/client-header.js
142
141
  var DEEPLAKE_CLIENT_HEADER = "X-Deeplake-Client";
@@ -147,6 +146,123 @@ function deeplakeClientHeader() {
147
146
  return { [DEEPLAKE_CLIENT_HEADER]: deeplakeClientValue() };
148
147
  }
149
148
 
149
+ // dist/src/deeplake-schema.js
150
+ var MEMORY_COLUMNS = Object.freeze([
151
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
152
+ { name: "path", sql: "TEXT NOT NULL DEFAULT ''" },
153
+ { name: "filename", sql: "TEXT NOT NULL DEFAULT ''" },
154
+ { name: "summary", sql: "TEXT NOT NULL DEFAULT ''" },
155
+ { name: "summary_embedding", sql: "FLOAT4[]" },
156
+ { name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
157
+ { name: "mime_type", sql: "TEXT NOT NULL DEFAULT 'text/plain'" },
158
+ { name: "size_bytes", sql: "BIGINT NOT NULL DEFAULT 0" },
159
+ { name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
160
+ { name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
161
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT ''" },
162
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" },
163
+ { name: "creation_date", sql: "TEXT NOT NULL DEFAULT ''" },
164
+ { name: "last_update_date", sql: "TEXT NOT NULL DEFAULT ''" }
165
+ ]);
166
+ var SESSIONS_COLUMNS = Object.freeze([
167
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
168
+ { name: "path", sql: "TEXT NOT NULL DEFAULT ''" },
169
+ { name: "filename", sql: "TEXT NOT NULL DEFAULT ''" },
170
+ { name: "message", sql: "JSONB" },
171
+ { name: "message_embedding", sql: "FLOAT4[]" },
172
+ { name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
173
+ { name: "mime_type", sql: "TEXT NOT NULL DEFAULT 'application/json'" },
174
+ { name: "size_bytes", sql: "BIGINT NOT NULL DEFAULT 0" },
175
+ { name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
176
+ { name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
177
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT ''" },
178
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" },
179
+ { name: "creation_date", sql: "TEXT NOT NULL DEFAULT ''" },
180
+ { name: "last_update_date", sql: "TEXT NOT NULL DEFAULT ''" }
181
+ ]);
182
+ var SKILLS_COLUMNS = Object.freeze([
183
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
184
+ { name: "name", sql: "TEXT NOT NULL DEFAULT ''" },
185
+ { name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
186
+ { name: "project_key", sql: "TEXT NOT NULL DEFAULT ''" },
187
+ { name: "local_path", sql: "TEXT NOT NULL DEFAULT ''" },
188
+ { name: "install", sql: "TEXT NOT NULL DEFAULT 'project'" },
189
+ { name: "source_sessions", sql: "TEXT NOT NULL DEFAULT '[]'" },
190
+ { name: "source_agent", sql: "TEXT NOT NULL DEFAULT ''" },
191
+ { name: "scope", sql: "TEXT NOT NULL DEFAULT 'me'" },
192
+ { name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
193
+ { name: "contributors", sql: "TEXT NOT NULL DEFAULT '[]'" },
194
+ { name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
195
+ { name: "trigger_text", sql: "TEXT NOT NULL DEFAULT ''" },
196
+ { name: "body", sql: "TEXT NOT NULL DEFAULT ''" },
197
+ { name: "version", sql: "BIGINT NOT NULL DEFAULT 1" },
198
+ { name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
199
+ { name: "updated_at", sql: "TEXT NOT NULL DEFAULT ''" }
200
+ ]);
201
+ function validateSchema(label, cols) {
202
+ const seen = /* @__PURE__ */ new Set();
203
+ for (const col of cols) {
204
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(col.name)) {
205
+ throw new Error(`${label}: column name "${col.name}" is not a valid SQL identifier`);
206
+ }
207
+ if (seen.has(col.name)) {
208
+ throw new Error(`${label}: duplicate column "${col.name}"`);
209
+ }
210
+ seen.add(col.name);
211
+ const notNull = /\bNOT\s+NULL\b/i.test(col.sql);
212
+ const hasDefault = /\bDEFAULT\b/i.test(col.sql);
213
+ if (notNull && !hasDefault) {
214
+ throw new Error(`${label}: column "${col.name}" is NOT NULL but has no DEFAULT \u2014 ALTER TABLE ADD COLUMN on a populated table would fail.`);
215
+ }
216
+ }
217
+ }
218
+ validateSchema("MEMORY_COLUMNS", MEMORY_COLUMNS);
219
+ validateSchema("SESSIONS_COLUMNS", SESSIONS_COLUMNS);
220
+ validateSchema("SKILLS_COLUMNS", SKILLS_COLUMNS);
221
+ function buildCreateTableSql(tableName, cols) {
222
+ const safe = sqlIdent(tableName);
223
+ const colSql = cols.map((c) => `${c.name} ${c.sql}`).join(", ");
224
+ return `CREATE TABLE IF NOT EXISTS "${safe}" (${colSql}) USING deeplake`;
225
+ }
226
+ function buildIntrospectionSql(tableName, workspaceId) {
227
+ return `SELECT column_name FROM information_schema.columns WHERE table_name = '${sqlStr(tableName)}' AND table_schema = '${sqlStr(workspaceId)}'`;
228
+ }
229
+ async function healMissingColumns(args) {
230
+ const safeTable = sqlIdent(args.tableName);
231
+ const introspectSql = buildIntrospectionSql(args.tableName, args.workspaceId);
232
+ const rows = await args.query(introspectSql);
233
+ const existing = /* @__PURE__ */ new Set();
234
+ for (const row of rows) {
235
+ const v = row?.column_name;
236
+ if (typeof v === "string")
237
+ existing.add(v.toLowerCase());
238
+ }
239
+ const missingCols = args.columns.filter((c) => !existing.has(c.name.toLowerCase()));
240
+ const missing = missingCols.map((c) => c.name);
241
+ if (missingCols.length === 0)
242
+ return { missing, altered: [] };
243
+ const altered = [];
244
+ for (const col of missingCols) {
245
+ try {
246
+ await args.query(`ALTER TABLE "${safeTable}" ADD COLUMN ${col.name} ${col.sql}`);
247
+ altered.push(col.name);
248
+ args.log?.(`schema-heal: added "${args.tableName}"."${col.name}"`);
249
+ } catch (e) {
250
+ const msg = e instanceof Error ? e.message : String(e);
251
+ if (!/already exists/i.test(msg))
252
+ throw e;
253
+ const recheck = await args.query(introspectSql);
254
+ const present = recheck.some((r) => {
255
+ const v = r?.column_name;
256
+ return typeof v === "string" && v.toLowerCase() === col.name.toLowerCase();
257
+ });
258
+ if (!present)
259
+ throw e;
260
+ args.log?.(`schema-heal: "${args.tableName}"."${col.name}" appeared via race, treating as success`);
261
+ }
262
+ }
263
+ return { missing, altered };
264
+ }
265
+
150
266
  // dist/src/notifications/queue.js
151
267
  import { readFileSync as readFileSync2, writeFileSync, renameSync, mkdirSync, openSync, closeSync, unlinkSync, statSync } from "node:fs";
152
268
  import { join as join3, resolve } from "node:path";
@@ -529,64 +645,33 @@ var DeeplakeApi = class {
529
645
  }
530
646
  }
531
647
  /**
532
- * Ensure a vector column exists on the given table.
533
- *
534
- * The previous implementation always issued `ALTER TABLE ADD COLUMN IF NOT
535
- * EXISTS …` on every SessionStart. On a long-running workspace that's
536
- * already migrated, every call returns 500 "Column already exists" — noisy
537
- * in the log and a wasted round-trip. Worse, the very first call after the
538
- * column is genuinely added triggers Deeplake's post-ALTER `vector::at`
539
- * window (~30s) during which subsequent INSERTs fail; minimising the
540
- * number of ALTER calls minimises exposure to that window.
541
- *
542
- * New flow:
543
- * 1. Check the local marker file (mirrors ensureLookupIndex). If fresh,
544
- * return — zero network calls.
545
- * 2. SELECT 1 FROM information_schema.columns WHERE table_name = T AND
546
- * column_name = C. Read-only, idempotent, can't tickle the post-ALTER
547
- * bug. If the column is present → mark + return.
548
- * 3. Only if step 2 says the column is missing, fall back to ALTER ADD
549
- * COLUMN IF NOT EXISTS. Mark on success, also mark if Deeplake reports
550
- * "already exists" (race: another client added it between our SELECT
551
- * and ALTER).
648
+ * Heal any missing columns on a table so it matches one of the schema
649
+ * definitions in `deeplake-schema.ts`. One SELECT against
650
+ * `information_schema.columns` per call, then `ALTER TABLE ADD COLUMN`
651
+ * only the genuinely missing ones never blanket, never `IF NOT
652
+ * EXISTS`.
552
653
  *
553
- * Marker uses the same dir / TTL as ensureLookupIndex so both schema
554
- * caches share an opt-out (HIVEMIND_INDEX_MARKER_DIR) and a TTL knob.
654
+ * History: an earlier path used a local marker file (`col_<name>` under
655
+ * the index-marker dir) to skip even the SELECT after the first
656
+ * confirmation, plus per-column ALTERs for `summary_embedding`,
657
+ * `message_embedding`, `agent`, `plugin_version`. The marker existed
658
+ * because Deeplake used to expose a ~30s post-ALTER bug where
659
+ * subsequent INSERTs failed, so we wanted to keep ALTER traffic to a
660
+ * minimum. The bug was re-verified on 2026-05-18 against
661
+ * `api.deeplake.ai` (`test_plugin` org) and no longer reproduces
662
+ * (71/71 INSERTs OK, first success 2ms after ALTER). The single SELECT
663
+ * + targeted ALTER pattern survives the marker removal because: each
664
+ * ALTER still costs ~800ms (so blanket sweeps are wasteful) and the
665
+ * diff produces clearer logs than "ALTER all with IF NOT EXISTS".
555
666
  */
556
- async ensureEmbeddingColumn(table, column) {
557
- await this.ensureColumn(table, column, "FLOAT4[]");
558
- }
559
- /**
560
- * Generic marker-gated column migration. Same SELECT-then-ALTER flow as
561
- * ensureEmbeddingColumn, parameterized by SQL type so it can patch up any
562
- * column that was added to the schema after the table was originally
563
- * created. Used today for `summary_embedding`, `message_embedding`, and
564
- * the `agent` column (added 2026-04-11) — the latter has no fallback if
565
- * a user upgraded over a pre-2026-04-11 table, so every INSERT fails
566
- * with `column "agent" does not exist`.
567
- */
568
- async ensureColumn(table, column, sqlType) {
569
- const markers = await getIndexMarkerStore();
570
- const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, `col_${column}`);
571
- if (markers.hasFreshIndexMarker(markerPath))
572
- return;
573
- const colCheck = `SELECT 1 FROM information_schema.columns WHERE table_name = '${sqlStr(table)}' AND column_name = '${sqlStr(column)}' AND table_schema = '${sqlStr(this.workspaceId)}' LIMIT 1`;
574
- const rows = await this.query(colCheck);
575
- if (rows.length > 0) {
576
- markers.writeIndexMarker(markerPath);
577
- return;
578
- }
579
- try {
580
- await this.query(`ALTER TABLE "${table}" ADD COLUMN ${column} ${sqlType}`);
581
- } catch (e) {
582
- const msg = e instanceof Error ? e.message : String(e);
583
- if (!/already exists/i.test(msg))
584
- throw e;
585
- const recheck = await this.query(colCheck);
586
- if (recheck.length === 0)
587
- throw e;
588
- }
589
- markers.writeIndexMarker(markerPath);
667
+ async healSchema(table, columns) {
668
+ await healMissingColumns({
669
+ query: (sql) => this.query(sql),
670
+ tableName: table,
671
+ workspaceId: this.workspaceId,
672
+ columns,
673
+ log: log3
674
+ });
590
675
  }
591
676
  /** List all tables in the workspace (with retry). */
592
677
  async listTables(forceRefresh = false) {
@@ -657,20 +742,21 @@ var DeeplakeApi = class {
657
742
  }
658
743
  throw lastErr;
659
744
  }
660
- /** Create the memory table if it doesn't already exist. Migrate columns on existing tables. */
745
+ /** Create the memory table if it doesn't already exist. Heal missing columns on existing tables. */
661
746
  async ensureTable(name) {
747
+ if (!MEMORY_COLUMNS.some((c) => c.name === SUMMARY_EMBEDDING_COL)) {
748
+ throw new Error(`MEMORY_COLUMNS missing "${SUMMARY_EMBEDDING_COL}" (embeddings/columns.ts drift)`);
749
+ }
662
750
  const tbl = sqlIdent(name ?? this.tableName);
663
751
  const tables = await this.listTables();
664
752
  if (!tables.includes(tbl)) {
665
753
  log3(`table "${tbl}" not found, creating`);
666
- await this.createTableWithRetry(`CREATE TABLE IF NOT EXISTS "${tbl}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', summary TEXT NOT NULL DEFAULT '', summary_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'text/plain', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', plugin_version TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`, tbl);
754
+ await this.createTableWithRetry(buildCreateTableSql(tbl, MEMORY_COLUMNS), tbl);
667
755
  log3(`table "${tbl}" created`);
668
756
  if (!tables.includes(tbl))
669
757
  this._tablesCache = [...tables, tbl];
670
758
  }
671
- await this.ensureEmbeddingColumn(tbl, SUMMARY_EMBEDDING_COL);
672
- await this.ensureColumn(tbl, "agent", "TEXT NOT NULL DEFAULT ''");
673
- await this.ensureColumn(tbl, "plugin_version", "TEXT NOT NULL DEFAULT ''");
759
+ await this.healSchema(tbl, MEMORY_COLUMNS);
674
760
  }
675
761
  /** Create the sessions table (uses JSONB for message since every row is a JSON event). */
676
762
  async ensureSessionsTable(name) {
@@ -678,14 +764,12 @@ var DeeplakeApi = class {
678
764
  const tables = await this.listTables();
679
765
  if (!tables.includes(safe)) {
680
766
  log3(`table "${safe}" not found, creating`);
681
- await this.createTableWithRetry(`CREATE TABLE IF NOT EXISTS "${safe}" (id TEXT NOT NULL DEFAULT '', path TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL DEFAULT '', message JSONB, message_embedding FLOAT4[], author TEXT NOT NULL DEFAULT '', mime_type TEXT NOT NULL DEFAULT 'application/json', size_bytes BIGINT NOT NULL DEFAULT 0, project TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', agent TEXT NOT NULL DEFAULT '', plugin_version TEXT NOT NULL DEFAULT '', creation_date TEXT NOT NULL DEFAULT '', last_update_date TEXT NOT NULL DEFAULT '') USING deeplake`, safe);
767
+ await this.createTableWithRetry(buildCreateTableSql(safe, SESSIONS_COLUMNS), safe);
682
768
  log3(`table "${safe}" created`);
683
769
  if (!tables.includes(safe))
684
770
  this._tablesCache = [...tables, safe];
685
771
  }
686
- await this.ensureEmbeddingColumn(safe, MESSAGE_EMBEDDING_COL);
687
- await this.ensureColumn(safe, "agent", "TEXT NOT NULL DEFAULT ''");
688
- await this.ensureColumn(safe, "plugin_version", "TEXT NOT NULL DEFAULT ''");
772
+ await this.healSchema(safe, SESSIONS_COLUMNS);
689
773
  await this.ensureLookupIndex(safe, "path_creation_date", `("path", "creation_date")`);
690
774
  }
691
775
  /**
@@ -703,11 +787,12 @@ var DeeplakeApi = class {
703
787
  const tables = await this.listTables();
704
788
  if (!tables.includes(safe)) {
705
789
  log3(`table "${safe}" not found, creating`);
706
- await this.createTableWithRetry(`CREATE TABLE IF NOT EXISTS "${safe}" (id TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '', project TEXT NOT NULL DEFAULT '', project_key TEXT NOT NULL DEFAULT '', local_path TEXT NOT NULL DEFAULT '', install TEXT NOT NULL DEFAULT 'project', source_sessions TEXT NOT NULL DEFAULT '[]', source_agent TEXT NOT NULL DEFAULT '', scope TEXT NOT NULL DEFAULT 'me', author TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '', trigger_text TEXT NOT NULL DEFAULT '', body TEXT NOT NULL DEFAULT '', version BIGINT NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL DEFAULT '') USING deeplake`, safe);
790
+ await this.createTableWithRetry(buildCreateTableSql(safe, SKILLS_COLUMNS), safe);
707
791
  log3(`table "${safe}" created`);
708
792
  if (!tables.includes(safe))
709
793
  this._tablesCache = [...tables, safe];
710
794
  }
795
+ await this.healSchema(safe, SKILLS_COLUMNS);
711
796
  await this.ensureLookupIndex(safe, "project_key_name", `("project_key", "name")`);
712
797
  }
713
798
  };