@deeplake/hivemind 0.7.35 → 0.7.37

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.
@@ -106,7 +106,121 @@ function sqlIdent(name) {
106
106
 
107
107
  // src/embeddings/columns.ts
108
108
  var SUMMARY_EMBEDDING_COL = "summary_embedding";
109
- var MESSAGE_EMBEDDING_COL = "message_embedding";
109
+
110
+ // src/deeplake-schema.ts
111
+ var MEMORY_COLUMNS = Object.freeze([
112
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
113
+ { name: "path", sql: "TEXT NOT NULL DEFAULT ''" },
114
+ { name: "filename", sql: "TEXT NOT NULL DEFAULT ''" },
115
+ { name: "summary", sql: "TEXT NOT NULL DEFAULT ''" },
116
+ { name: "summary_embedding", sql: "FLOAT4[]" },
117
+ { name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
118
+ { name: "mime_type", sql: "TEXT NOT NULL DEFAULT 'text/plain'" },
119
+ { name: "size_bytes", sql: "BIGINT NOT NULL DEFAULT 0" },
120
+ { name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
121
+ { name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
122
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT ''" },
123
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" },
124
+ { name: "creation_date", sql: "TEXT NOT NULL DEFAULT ''" },
125
+ { name: "last_update_date", sql: "TEXT NOT NULL DEFAULT ''" }
126
+ ]);
127
+ var SESSIONS_COLUMNS = Object.freeze([
128
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
129
+ { name: "path", sql: "TEXT NOT NULL DEFAULT ''" },
130
+ { name: "filename", sql: "TEXT NOT NULL DEFAULT ''" },
131
+ { name: "message", sql: "JSONB" },
132
+ { name: "message_embedding", sql: "FLOAT4[]" },
133
+ { name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
134
+ { name: "mime_type", sql: "TEXT NOT NULL DEFAULT 'application/json'" },
135
+ { name: "size_bytes", sql: "BIGINT NOT NULL DEFAULT 0" },
136
+ { name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
137
+ { name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
138
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT ''" },
139
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" },
140
+ { name: "creation_date", sql: "TEXT NOT NULL DEFAULT ''" },
141
+ { name: "last_update_date", sql: "TEXT NOT NULL DEFAULT ''" }
142
+ ]);
143
+ var SKILLS_COLUMNS = Object.freeze([
144
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
145
+ { name: "name", sql: "TEXT NOT NULL DEFAULT ''" },
146
+ { name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
147
+ { name: "project_key", sql: "TEXT NOT NULL DEFAULT ''" },
148
+ { name: "local_path", sql: "TEXT NOT NULL DEFAULT ''" },
149
+ { name: "install", sql: "TEXT NOT NULL DEFAULT 'project'" },
150
+ { name: "source_sessions", sql: "TEXT NOT NULL DEFAULT '[]'" },
151
+ { name: "source_agent", sql: "TEXT NOT NULL DEFAULT ''" },
152
+ { name: "scope", sql: "TEXT NOT NULL DEFAULT 'me'" },
153
+ { name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
154
+ { name: "contributors", sql: "TEXT NOT NULL DEFAULT '[]'" },
155
+ { name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
156
+ { name: "trigger_text", sql: "TEXT NOT NULL DEFAULT ''" },
157
+ { name: "body", sql: "TEXT NOT NULL DEFAULT ''" },
158
+ { name: "version", sql: "BIGINT NOT NULL DEFAULT 1" },
159
+ { name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
160
+ { name: "updated_at", sql: "TEXT NOT NULL DEFAULT ''" }
161
+ ]);
162
+ function validateSchema(label, cols) {
163
+ const seen = /* @__PURE__ */ new Set();
164
+ for (const col of cols) {
165
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(col.name)) {
166
+ throw new Error(`${label}: column name "${col.name}" is not a valid SQL identifier`);
167
+ }
168
+ if (seen.has(col.name)) {
169
+ throw new Error(`${label}: duplicate column "${col.name}"`);
170
+ }
171
+ seen.add(col.name);
172
+ const notNull = /\bNOT\s+NULL\b/i.test(col.sql);
173
+ const hasDefault = /\bDEFAULT\b/i.test(col.sql);
174
+ if (notNull && !hasDefault) {
175
+ throw new Error(
176
+ `${label}: column "${col.name}" is NOT NULL but has no DEFAULT \u2014 ALTER TABLE ADD COLUMN on a populated table would fail.`
177
+ );
178
+ }
179
+ }
180
+ }
181
+ validateSchema("MEMORY_COLUMNS", MEMORY_COLUMNS);
182
+ validateSchema("SESSIONS_COLUMNS", SESSIONS_COLUMNS);
183
+ validateSchema("SKILLS_COLUMNS", SKILLS_COLUMNS);
184
+ function buildCreateTableSql(tableName, cols) {
185
+ const safe = sqlIdent(tableName);
186
+ const colSql = cols.map((c) => `${c.name} ${c.sql}`).join(", ");
187
+ return `CREATE TABLE IF NOT EXISTS "${safe}" (${colSql}) USING deeplake`;
188
+ }
189
+ function buildIntrospectionSql(tableName, workspaceId) {
190
+ return `SELECT column_name FROM information_schema.columns WHERE table_name = '${sqlStr(tableName)}' AND table_schema = '${sqlStr(workspaceId)}'`;
191
+ }
192
+ async function healMissingColumns(args) {
193
+ const safeTable = sqlIdent(args.tableName);
194
+ const introspectSql = buildIntrospectionSql(args.tableName, args.workspaceId);
195
+ const rows = await args.query(introspectSql);
196
+ const existing = /* @__PURE__ */ new Set();
197
+ for (const row of rows) {
198
+ const v = row?.column_name;
199
+ if (typeof v === "string") existing.add(v.toLowerCase());
200
+ }
201
+ const missingCols = args.columns.filter((c) => !existing.has(c.name.toLowerCase()));
202
+ const missing = missingCols.map((c) => c.name);
203
+ if (missingCols.length === 0) return { missing, altered: [] };
204
+ const altered = [];
205
+ for (const col of missingCols) {
206
+ try {
207
+ await args.query(`ALTER TABLE "${safeTable}" ADD COLUMN ${col.name} ${col.sql}`);
208
+ altered.push(col.name);
209
+ args.log?.(`schema-heal: added "${args.tableName}"."${col.name}"`);
210
+ } catch (e) {
211
+ const msg = e instanceof Error ? e.message : String(e);
212
+ if (!/already exists/i.test(msg)) throw e;
213
+ const recheck = await args.query(introspectSql);
214
+ const present = recheck.some((r) => {
215
+ const v = r?.column_name;
216
+ return typeof v === "string" && v.toLowerCase() === col.name.toLowerCase();
217
+ });
218
+ if (!present) throw e;
219
+ args.log?.(`schema-heal: "${args.tableName}"."${col.name}" appeared via race, treating as success`);
220
+ }
221
+ }
222
+ return { missing, altered };
223
+ }
110
224
 
111
225
  // src/notifications/queue.ts
112
226
  import { readFileSync, writeFileSync, renameSync, mkdirSync, openSync, closeSync, unlinkSync, statSync } from "node:fs";
@@ -469,61 +583,33 @@ var DeeplakeApi = class {
469
583
  }
470
584
  }
471
585
  /**
472
- * Ensure a vector column exists on the given table.
586
+ * Heal any missing columns on a table so it matches one of the schema
587
+ * definitions in `deeplake-schema.ts`. One SELECT against
588
+ * `information_schema.columns` per call, then `ALTER TABLE ADD COLUMN`
589
+ * only the genuinely missing ones — never blanket, never `IF NOT
590
+ * EXISTS`.
473
591
  *
474
- * The previous implementation always issued `ALTER TABLE ADD COLUMN IF NOT
475
- * EXISTS …` on every SessionStart. On a long-running workspace that's
476
- * already migrated, every call returns 500 "Column already exists" — noisy
477
- * in the log and a wasted round-trip. Worse, the very first call after the
478
- * column is genuinely added triggers Deeplake's post-ALTER `vector::at`
479
- * window (~30s) during which subsequent INSERTs fail; minimising the
480
- * number of ALTER calls minimises exposure to that window.
481
- *
482
- * New flow:
483
- * 1. Check the local marker file (mirrors ensureLookupIndex). If fresh,
484
- * return zero network calls.
485
- * 2. SELECT 1 FROM information_schema.columns WHERE table_name = T AND
486
- * column_name = C. Read-only, idempotent, can't tickle the post-ALTER
487
- * bug. If the column is present → mark + return.
488
- * 3. Only if step 2 says the column is missing, fall back to ALTER ADD
489
- * COLUMN IF NOT EXISTS. Mark on success, also mark if Deeplake reports
490
- * "already exists" (race: another client added it between our SELECT
491
- * and ALTER).
492
- *
493
- * Marker uses the same dir / TTL as ensureLookupIndex so both schema
494
- * caches share an opt-out (HIVEMIND_INDEX_MARKER_DIR) and a TTL knob.
495
- */
496
- async ensureEmbeddingColumn(table, column) {
497
- await this.ensureColumn(table, column, "FLOAT4[]");
498
- }
499
- /**
500
- * Generic marker-gated column migration. Same SELECT-then-ALTER flow as
501
- * ensureEmbeddingColumn, parameterized by SQL type so it can patch up any
502
- * column that was added to the schema after the table was originally
503
- * created. Used today for `summary_embedding`, `message_embedding`, and
504
- * the `agent` column (added 2026-04-11) — the latter has no fallback if
505
- * a user upgraded over a pre-2026-04-11 table, so every INSERT fails
506
- * with `column "agent" does not exist`.
592
+ * History: an earlier path used a local marker file (`col_<name>` under
593
+ * the index-marker dir) to skip even the SELECT after the first
594
+ * confirmation, plus per-column ALTERs for `summary_embedding`,
595
+ * `message_embedding`, `agent`, `plugin_version`. The marker existed
596
+ * because Deeplake used to expose a ~30s post-ALTER bug where
597
+ * subsequent INSERTs failed, so we wanted to keep ALTER traffic to a
598
+ * minimum. The bug was re-verified on 2026-05-18 against
599
+ * `api.deeplake.ai` (`test_plugin` org) and no longer reproduces
600
+ * (71/71 INSERTs OK, first success 2ms after ALTER). The single SELECT
601
+ * + targeted ALTER pattern survives the marker removal because: each
602
+ * ALTER still costs ~800ms (so blanket sweeps are wasteful) and the
603
+ * diff produces clearer logs than "ALTER all with IF NOT EXISTS".
507
604
  */
508
- async ensureColumn(table, column, sqlType) {
509
- const markers = await getIndexMarkerStore();
510
- const markerPath = markers.buildIndexMarkerPath(this.workspaceId, this.orgId, table, `col_${column}`);
511
- if (markers.hasFreshIndexMarker(markerPath)) return;
512
- 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`;
513
- const rows = await this.query(colCheck);
514
- if (rows.length > 0) {
515
- markers.writeIndexMarker(markerPath);
516
- return;
517
- }
518
- try {
519
- await this.query(`ALTER TABLE "${table}" ADD COLUMN ${column} ${sqlType}`);
520
- } catch (e) {
521
- const msg = e instanceof Error ? e.message : String(e);
522
- if (!/already exists/i.test(msg)) throw e;
523
- const recheck = await this.query(colCheck);
524
- if (recheck.length === 0) throw e;
525
- }
526
- markers.writeIndexMarker(markerPath);
605
+ async healSchema(table, columns) {
606
+ await healMissingColumns({
607
+ query: (sql) => this.query(sql),
608
+ tableName: table,
609
+ workspaceId: this.workspaceId,
610
+ columns,
611
+ log: log3
612
+ });
527
613
  }
528
614
  /** List all tables in the workspace (with retry). */
529
615
  async listTables(forceRefresh = false) {
@@ -592,22 +678,20 @@ var DeeplakeApi = class {
592
678
  }
593
679
  throw lastErr;
594
680
  }
595
- /** Create the memory table if it doesn't already exist. Migrate columns on existing tables. */
681
+ /** Create the memory table if it doesn't already exist. Heal missing columns on existing tables. */
596
682
  async ensureTable(name) {
683
+ if (!MEMORY_COLUMNS.some((c) => c.name === SUMMARY_EMBEDDING_COL)) {
684
+ throw new Error(`MEMORY_COLUMNS missing "${SUMMARY_EMBEDDING_COL}" (embeddings/columns.ts drift)`);
685
+ }
597
686
  const tbl = sqlIdent(name ?? this.tableName);
598
687
  const tables = await this.listTables();
599
688
  if (!tables.includes(tbl)) {
600
689
  log3(`table "${tbl}" not found, creating`);
601
- await this.createTableWithRetry(
602
- `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`,
603
- tbl
604
- );
690
+ await this.createTableWithRetry(buildCreateTableSql(tbl, MEMORY_COLUMNS), tbl);
605
691
  log3(`table "${tbl}" created`);
606
692
  if (!tables.includes(tbl)) this._tablesCache = [...tables, tbl];
607
693
  }
608
- await this.ensureEmbeddingColumn(tbl, SUMMARY_EMBEDDING_COL);
609
- await this.ensureColumn(tbl, "agent", "TEXT NOT NULL DEFAULT ''");
610
- await this.ensureColumn(tbl, "plugin_version", "TEXT NOT NULL DEFAULT ''");
694
+ await this.healSchema(tbl, MEMORY_COLUMNS);
611
695
  }
612
696
  /** Create the sessions table (uses JSONB for message since every row is a JSON event). */
613
697
  async ensureSessionsTable(name) {
@@ -615,16 +699,11 @@ var DeeplakeApi = class {
615
699
  const tables = await this.listTables();
616
700
  if (!tables.includes(safe)) {
617
701
  log3(`table "${safe}" not found, creating`);
618
- await this.createTableWithRetry(
619
- `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`,
620
- safe
621
- );
702
+ await this.createTableWithRetry(buildCreateTableSql(safe, SESSIONS_COLUMNS), safe);
622
703
  log3(`table "${safe}" created`);
623
704
  if (!tables.includes(safe)) this._tablesCache = [...tables, safe];
624
705
  }
625
- await this.ensureEmbeddingColumn(safe, MESSAGE_EMBEDDING_COL);
626
- await this.ensureColumn(safe, "agent", "TEXT NOT NULL DEFAULT ''");
627
- await this.ensureColumn(safe, "plugin_version", "TEXT NOT NULL DEFAULT ''");
706
+ await this.healSchema(safe, SESSIONS_COLUMNS);
628
707
  await this.ensureLookupIndex(safe, "path_creation_date", `("path", "creation_date")`);
629
708
  }
630
709
  /**
@@ -642,13 +721,11 @@ var DeeplakeApi = class {
642
721
  const tables = await this.listTables();
643
722
  if (!tables.includes(safe)) {
644
723
  log3(`table "${safe}" not found, creating`);
645
- await this.createTableWithRetry(
646
- `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`,
647
- safe
648
- );
724
+ await this.createTableWithRetry(buildCreateTableSql(safe, SKILLS_COLUMNS), safe);
649
725
  log3(`table "${safe}" created`);
650
726
  if (!tables.includes(safe)) this._tablesCache = [...tables, safe];
651
727
  }
728
+ await this.healSchema(safe, SKILLS_COLUMNS);
652
729
  await this.ensureLookupIndex(safe, "project_key_name", `("project_key", "name")`);
653
730
  }
654
731
  };
@@ -1466,7 +1543,7 @@ function extractLatestVersion(body) {
1466
1543
  return typeof v === "string" && v.length > 0 ? v : null;
1467
1544
  }
1468
1545
  function getInstalledVersion() {
1469
- return "0.7.35".length > 0 ? "0.7.35" : null;
1546
+ return "0.7.37".length > 0 ? "0.7.37" : null;
1470
1547
  }
1471
1548
  function isNewer(latest, current) {
1472
1549
  const parse = (v) => v.replace(/-.*$/, "").split(".").map(Number);
@@ -317,6 +317,9 @@ ${s.body}
317
317
  import { randomUUID } from "node:crypto";
318
318
 
319
319
  // dist/src/utils/sql.js
320
+ function sqlStr(value) {
321
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/\0/g, "").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
322
+ }
320
323
  function sqlIdent(name) {
321
324
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
322
325
  throw new Error(`Invalid SQL identifier: ${JSON.stringify(name)}`);
@@ -324,29 +327,142 @@ function sqlIdent(name) {
324
327
  return name;
325
328
  }
326
329
 
327
- // dist/src/skillify/skills-table.js
328
- function createSkillsTableSql(tableName) {
329
- const safe = sqlIdent(tableName);
330
- return `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 '', contributors 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`;
330
+ // dist/src/deeplake-schema.js
331
+ var MEMORY_COLUMNS = Object.freeze([
332
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
333
+ { name: "path", sql: "TEXT NOT NULL DEFAULT ''" },
334
+ { name: "filename", sql: "TEXT NOT NULL DEFAULT ''" },
335
+ { name: "summary", sql: "TEXT NOT NULL DEFAULT ''" },
336
+ { name: "summary_embedding", sql: "FLOAT4[]" },
337
+ { name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
338
+ { name: "mime_type", sql: "TEXT NOT NULL DEFAULT 'text/plain'" },
339
+ { name: "size_bytes", sql: "BIGINT NOT NULL DEFAULT 0" },
340
+ { name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
341
+ { name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
342
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT ''" },
343
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" },
344
+ { name: "creation_date", sql: "TEXT NOT NULL DEFAULT ''" },
345
+ { name: "last_update_date", sql: "TEXT NOT NULL DEFAULT ''" }
346
+ ]);
347
+ var SESSIONS_COLUMNS = Object.freeze([
348
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
349
+ { name: "path", sql: "TEXT NOT NULL DEFAULT ''" },
350
+ { name: "filename", sql: "TEXT NOT NULL DEFAULT ''" },
351
+ { name: "message", sql: "JSONB" },
352
+ { name: "message_embedding", sql: "FLOAT4[]" },
353
+ { name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
354
+ { name: "mime_type", sql: "TEXT NOT NULL DEFAULT 'application/json'" },
355
+ { name: "size_bytes", sql: "BIGINT NOT NULL DEFAULT 0" },
356
+ { name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
357
+ { name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
358
+ { name: "agent", sql: "TEXT NOT NULL DEFAULT ''" },
359
+ { name: "plugin_version", sql: "TEXT NOT NULL DEFAULT ''" },
360
+ { name: "creation_date", sql: "TEXT NOT NULL DEFAULT ''" },
361
+ { name: "last_update_date", sql: "TEXT NOT NULL DEFAULT ''" }
362
+ ]);
363
+ var SKILLS_COLUMNS = Object.freeze([
364
+ { name: "id", sql: "TEXT NOT NULL DEFAULT ''" },
365
+ { name: "name", sql: "TEXT NOT NULL DEFAULT ''" },
366
+ { name: "project", sql: "TEXT NOT NULL DEFAULT ''" },
367
+ { name: "project_key", sql: "TEXT NOT NULL DEFAULT ''" },
368
+ { name: "local_path", sql: "TEXT NOT NULL DEFAULT ''" },
369
+ { name: "install", sql: "TEXT NOT NULL DEFAULT 'project'" },
370
+ { name: "source_sessions", sql: "TEXT NOT NULL DEFAULT '[]'" },
371
+ { name: "source_agent", sql: "TEXT NOT NULL DEFAULT ''" },
372
+ { name: "scope", sql: "TEXT NOT NULL DEFAULT 'me'" },
373
+ { name: "author", sql: "TEXT NOT NULL DEFAULT ''" },
374
+ { name: "contributors", sql: "TEXT NOT NULL DEFAULT '[]'" },
375
+ { name: "description", sql: "TEXT NOT NULL DEFAULT ''" },
376
+ { name: "trigger_text", sql: "TEXT NOT NULL DEFAULT ''" },
377
+ { name: "body", sql: "TEXT NOT NULL DEFAULT ''" },
378
+ { name: "version", sql: "BIGINT NOT NULL DEFAULT 1" },
379
+ { name: "created_at", sql: "TEXT NOT NULL DEFAULT ''" },
380
+ { name: "updated_at", sql: "TEXT NOT NULL DEFAULT ''" }
381
+ ]);
382
+ function validateSchema(label, cols) {
383
+ const seen = /* @__PURE__ */ new Set();
384
+ for (const col of cols) {
385
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(col.name)) {
386
+ throw new Error(`${label}: column name "${col.name}" is not a valid SQL identifier`);
387
+ }
388
+ if (seen.has(col.name)) {
389
+ throw new Error(`${label}: duplicate column "${col.name}"`);
390
+ }
391
+ seen.add(col.name);
392
+ const notNull = /\bNOT\s+NULL\b/i.test(col.sql);
393
+ const hasDefault = /\bDEFAULT\b/i.test(col.sql);
394
+ if (notNull && !hasDefault) {
395
+ 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.`);
396
+ }
397
+ }
331
398
  }
332
- function addContributorsColumnSql(tableName) {
399
+ validateSchema("MEMORY_COLUMNS", MEMORY_COLUMNS);
400
+ validateSchema("SESSIONS_COLUMNS", SESSIONS_COLUMNS);
401
+ validateSchema("SKILLS_COLUMNS", SKILLS_COLUMNS);
402
+ function buildCreateTableSql(tableName, cols) {
333
403
  const safe = sqlIdent(tableName);
334
- return `ALTER TABLE "${safe}" ADD COLUMN IF NOT EXISTS contributors TEXT NOT NULL DEFAULT '[]'`;
404
+ const colSql = cols.map((c) => `${c.name} ${c.sql}`).join(", ");
405
+ return `CREATE TABLE IF NOT EXISTS "${safe}" (${colSql}) USING deeplake`;
335
406
  }
336
- function esc(s) {
337
- return s.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
407
+ function buildIntrospectionSql(tableName, workspaceId) {
408
+ return `SELECT column_name FROM information_schema.columns WHERE table_name = '${sqlStr(tableName)}' AND table_schema = '${sqlStr(workspaceId)}'`;
409
+ }
410
+ async function healMissingColumns(args) {
411
+ const safeTable = sqlIdent(args.tableName);
412
+ const introspectSql = buildIntrospectionSql(args.tableName, args.workspaceId);
413
+ const rows = await args.query(introspectSql);
414
+ const existing = /* @__PURE__ */ new Set();
415
+ for (const row of rows) {
416
+ const v = row?.column_name;
417
+ if (typeof v === "string")
418
+ existing.add(v.toLowerCase());
419
+ }
420
+ const missingCols = args.columns.filter((c) => !existing.has(c.name.toLowerCase()));
421
+ const missing = missingCols.map((c) => c.name);
422
+ if (missingCols.length === 0)
423
+ return { missing, altered: [] };
424
+ const altered = [];
425
+ for (const col of missingCols) {
426
+ try {
427
+ await args.query(`ALTER TABLE "${safeTable}" ADD COLUMN ${col.name} ${col.sql}`);
428
+ altered.push(col.name);
429
+ args.log?.(`schema-heal: added "${args.tableName}"."${col.name}"`);
430
+ } catch (e) {
431
+ const msg = e instanceof Error ? e.message : String(e);
432
+ if (!/already exists/i.test(msg))
433
+ throw e;
434
+ const recheck = await args.query(introspectSql);
435
+ const present = recheck.some((r) => {
436
+ const v = r?.column_name;
437
+ return typeof v === "string" && v.toLowerCase() === col.name.toLowerCase();
438
+ });
439
+ if (!present)
440
+ throw e;
441
+ args.log?.(`schema-heal: "${args.tableName}"."${col.name}" appeared via race, treating as success`);
442
+ }
443
+ }
444
+ return { missing, altered };
338
445
  }
339
446
  function isMissingTableError(message) {
340
447
  if (!message)
341
448
  return false;
449
+ if (/permission denied|must be owner/i.test(message))
450
+ return false;
342
451
  if (/\bcolumn\b/i.test(message))
343
452
  return false;
344
453
  return /Table does not exist|relation .* does not exist|no such table/i.test(message);
345
454
  }
346
- function isMissingContributorsColumnError(message) {
455
+ function isMissingColumnError(message) {
347
456
  if (!message)
348
457
  return false;
349
- return /contributors.*(?:does not exist|not found|unknown)/i.test(message) || /(?:does not exist|unknown column).*contributors/i.test(message);
458
+ if (/permission denied|must be owner/i.test(message))
459
+ return false;
460
+ return /column ["']?[A-Za-z_][A-Za-z0-9_]*["']? .*does not exist/i.test(message) || /unknown column/i.test(message) || /no such column/i.test(message);
461
+ }
462
+
463
+ // dist/src/skillify/skills-table.js
464
+ function esc(s) {
465
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "''").replace(/[\x01-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
350
466
  }
351
467
  async function insertSkillRow(args) {
352
468
  const id = args.id ?? randomUUID();
@@ -355,14 +471,29 @@ async function insertSkillRow(args) {
355
471
  const sql = `INSERT INTO "${sqlIdent(args.tableName)}" (id, name, project, project_key, local_path, install, source_sessions, source_agent, scope, author, contributors, description, trigger_text, body, version, created_at, updated_at) VALUES ('${esc(id)}', '${esc(args.name)}', '${esc(args.project)}', '${esc(args.projectKey)}', '${esc(args.localPath)}', '${esc(args.install)}', '${esc(sourceSessionsJson)}', '${esc(args.sourceAgent)}', '${esc(args.scope)}', '${esc(args.author)}', '${esc(contributorsJson)}', '${esc(args.description)}', '${esc(args.trigger ?? "")}', '${esc(args.body)}', ${args.version}, '${esc(args.createdAt)}', '${esc(args.updatedAt)}')`;
356
472
  try {
357
473
  await args.query(sql);
474
+ return;
358
475
  } catch (e) {
359
- if (isMissingTableError(e?.message)) {
360
- await args.query(createSkillsTableSql(args.tableName));
476
+ const msg = e instanceof Error ? e.message : String(e);
477
+ if (isMissingTableError(msg)) {
478
+ await args.query(buildCreateTableSql(args.tableName, SKILLS_COLUMNS));
479
+ await healMissingColumns({
480
+ query: args.query,
481
+ tableName: args.tableName,
482
+ workspaceId: args.workspaceId,
483
+ columns: SKILLS_COLUMNS
484
+ });
361
485
  await args.query(sql);
362
486
  return;
363
487
  }
364
- if (isMissingContributorsColumnError(e?.message)) {
365
- await args.query(addContributorsColumnSql(args.tableName));
488
+ if (isMissingColumnError(msg)) {
489
+ const result = await healMissingColumns({
490
+ query: args.query,
491
+ tableName: args.tableName,
492
+ workspaceId: args.workspaceId,
493
+ columns: SKILLS_COLUMNS
494
+ });
495
+ if (result.missing.length === 0)
496
+ throw e;
366
497
  await args.query(sql);
367
498
  return;
368
499
  }
@@ -1002,6 +1133,7 @@ async function main() {
1002
1133
  await insertSkillRow({
1003
1134
  query,
1004
1135
  tableName: cfg.skillsTable,
1136
+ workspaceId: cfg.workspaceId,
1005
1137
  name: verdict2.name,
1006
1138
  project: cfg.project,
1007
1139
  projectKey: cfg.projectKey,
@@ -52,5 +52,5 @@
52
52
  }
53
53
  }
54
54
  },
55
- "version": "0.7.35"
55
+ "version": "0.7.37"
56
56
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hivemind",
3
- "version": "0.7.35",
3
+ "version": "0.7.37",
4
4
  "type": "module",
5
5
  "description": "Hivemind — cloud-backed persistent shared memory for AI agents, powered by DeepLake",
6
6
  "license": "Apache-2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeplake/hivemind",
3
- "version": "0.7.35",
3
+ "version": "0.7.37",
4
4
  "description": "Cloud-backed persistent shared memory for AI agents powered by Deeplake",
5
5
  "type": "module",
6
6
  "repository": {
@@ -30,6 +30,7 @@
30
30
  "LICENSE"
31
31
  ],
32
32
  "scripts": {
33
+ "prebuild": "node scripts/sync-versions.mjs",
33
34
  "build": "tsc && node esbuild.config.mjs",
34
35
  "bundle": "node esbuild.config.mjs",
35
36
  "dev": "tsc --watch",
@@ -41,8 +42,8 @@
41
42
  "audit:openclaw": "node scripts/audit-openclaw-bundle.mjs",
42
43
  "pack:check": "node scripts/pack-check.mjs",
43
44
  "ci": "npm run typecheck && npm run dup && npm test",
44
- "prepare": "husky",
45
- "prepublishOnly": "npm run build"
45
+ "prepare": "husky && npm run build",
46
+ "prepack": "npm run build"
46
47
  },
47
48
  "lint-staged": {
48
49
  "*.ts": [