@axiom-lattice/local-stores 3.0.2 → 3.1.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.mjs CHANGED
@@ -588,6 +588,7 @@ function mapRowToWorkspace(row) {
588
588
  }
589
589
 
590
590
  // src/stores/LocalProjectStore.ts
591
+ import { assertGenericProjectConfig } from "@axiom-lattice/protocols";
591
592
  var DDL4 = `
592
593
  CREATE TABLE IF NOT EXISTS lt_projects (
593
594
  id TEXT NOT NULL,
@@ -628,6 +629,7 @@ var LocalProjectStore = class {
628
629
  return row ? mapRowToProject(row) : null;
629
630
  }
630
631
  async createProject(tenantId, workspaceId, id, data) {
632
+ assertGenericProjectConfig(data.config);
631
633
  const now = nowISO();
632
634
  const kind = data.kind || "business";
633
635
  this.db.prepare(
@@ -637,23 +639,18 @@ var LocalProjectStore = class {
637
639
  workspace_id = excluded.workspace_id,
638
640
  name = excluded.name,
639
641
  description = excluded.description,
640
- config = excluded.config,
642
+ config = CASE
643
+ WHEN json_type(lt_projects.config, '$.capabilityBundleIds') IS NOT NULL
644
+ THEN json_set(COALESCE(excluded.config, '{}'), '$.capabilityBundleIds', json_extract(lt_projects.config, '$.capabilityBundleIds'))
645
+ ELSE excluded.config
646
+ END,
641
647
  kind = excluded.kind,
642
648
  updated_at = excluded.updated_at`
643
649
  ).run(id, tenantId, workspaceId, data.name, data.description || null, data.config ? JSON.stringify(data.config) : null, kind, now, now);
644
- return {
645
- id,
646
- tenantId,
647
- workspaceId,
648
- name: data.name,
649
- description: data.description,
650
- config: data.config,
651
- kind,
652
- createdAt: parseISO(now),
653
- updatedAt: parseISO(now)
654
- };
650
+ return await this.getProjectById(tenantId, id);
655
651
  }
656
652
  async updateProject(tenantId, id, updates) {
653
+ assertGenericProjectConfig(updates.config);
657
654
  const existing = await this.getProjectById(tenantId, id);
658
655
  if (!existing) return null;
659
656
  const setClauses = [];
@@ -667,8 +664,13 @@ var LocalProjectStore = class {
667
664
  values.push(updates.description || null);
668
665
  }
669
666
  if (updates.config !== void 0) {
670
- setClauses.push("config = ?");
671
- values.push(updates.config ? JSON.stringify(updates.config) : null);
667
+ setClauses.push(`config = CASE
668
+ WHEN json_type(config, '$.capabilityBundleIds') IS NOT NULL
669
+ THEN json_set(json(?), '$.capabilityBundleIds', json_extract(config, '$.capabilityBundleIds'))
670
+ ELSE ?
671
+ END`);
672
+ const config = JSON.stringify(updates.config ?? {});
673
+ values.push(config, config);
672
674
  }
673
675
  if (updates.kind !== void 0) {
674
676
  setClauses.push("kind = ?");
@@ -690,6 +692,52 @@ var LocalProjectStore = class {
690
692
  ).run(tenantId, id);
691
693
  return result.changes > 0;
692
694
  }
695
+ async updateCapabilityBundleIds(tenantId, projectId, bundleIds, expectedRevisions = {}) {
696
+ const now = nowISO();
697
+ const result = this.db.prepare(
698
+ `UPDATE lt_projects
699
+ SET config = json_set(COALESCE(config, '{}'), '$.capabilityBundleIds', json(?)), updated_at = ?
700
+ WHERE tenant_id = ? AND id = ?
701
+ AND NOT EXISTS (
702
+ SELECT 1 FROM json_each(?) AS requested
703
+ WHERE NOT EXISTS (
704
+ SELECT 1 FROM lt_capability_bundles
705
+ WHERE tenant_id = ? AND id = requested.value
706
+ )
707
+ )
708
+ AND NOT EXISTS (
709
+ SELECT 1 FROM json_each(?) AS expected
710
+ WHERE EXISTS (
711
+ SELECT 1 FROM lt_capability_bundles
712
+ WHERE tenant_id = ? AND id = expected.key AND updated_at <> expected.value
713
+ )
714
+ )`
715
+ ).run(JSON.stringify(bundleIds), now, tenantId, projectId, JSON.stringify(bundleIds), tenantId, JSON.stringify(expectedRevisions), tenantId);
716
+ if (result.changes > 0) {
717
+ const project = await this.getProjectById(tenantId, projectId);
718
+ return project ? { status: "updated", project } : { status: "project_not_found" };
719
+ }
720
+ const revisionMismatch = this.db.prepare(
721
+ "SELECT 1 FROM lt_capability_bundles WHERE tenant_id = ? AND id IN (SELECT key FROM json_each(?)) AND updated_at <> (SELECT value FROM json_each(?) WHERE key = lt_capability_bundles.id) LIMIT 1"
722
+ ).get(tenantId, JSON.stringify(expectedRevisions), JSON.stringify(expectedRevisions));
723
+ if (revisionMismatch) return { status: "bundle_conflict" };
724
+ return await this.getProjectById(tenantId, projectId) ? { status: "bundle_not_found" } : { status: "project_not_found" };
725
+ }
726
+ async isCapabilityBundleReferenced(tenantId, bundleId) {
727
+ const rows = this.db.prepare("SELECT config FROM lt_projects WHERE tenant_id = ?").all(tenantId);
728
+ return rows.some((row) => {
729
+ if (!row.config) return false;
730
+ let config;
731
+ try {
732
+ config = JSON.parse(row.config);
733
+ } catch {
734
+ return false;
735
+ }
736
+ if (config === null || typeof config !== "object" || Array.isArray(config)) return false;
737
+ const ids = config.capabilityBundleIds;
738
+ return Array.isArray(ids) && ids.every((id) => typeof id === "string") && ids.includes(bundleId);
739
+ });
740
+ }
693
741
  /** Add a column if it does not exist (SQLite version compatible). */
694
742
  ensureColumn(table, column, ddl) {
695
743
  const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
@@ -2168,8 +2216,34 @@ var LocalEvalStore = class {
2168
2216
  return this.getProjectById(tenantId, id);
2169
2217
  }
2170
2218
  async deleteProject(tenantId, id) {
2171
- const result = this.db.prepare(`DELETE FROM lt_eval_projects WHERE tenant_id = ? AND id = ?`).run(tenantId, id);
2172
- return result.changes > 0;
2219
+ const db = this.db.getRawDb();
2220
+ db.run("BEGIN TRANSACTION");
2221
+ try {
2222
+ db.run(
2223
+ `DELETE FROM lt_eval_run_results
2224
+ WHERE run_id IN (
2225
+ SELECT id FROM lt_eval_runs WHERE tenant_id = ? AND project_id = ?
2226
+ )`,
2227
+ [tenantId, id]
2228
+ );
2229
+ db.run(
2230
+ `DELETE FROM lt_eval_cases
2231
+ WHERE tenant_id = ? AND suite_id IN (
2232
+ SELECT id FROM lt_eval_suites WHERE tenant_id = ? AND project_id = ?
2233
+ )`,
2234
+ [tenantId, tenantId, id]
2235
+ );
2236
+ db.run(`DELETE FROM lt_eval_runs WHERE tenant_id = ? AND project_id = ?`, [tenantId, id]);
2237
+ db.run(`DELETE FROM lt_eval_suites WHERE tenant_id = ? AND project_id = ?`, [tenantId, id]);
2238
+ db.run(`DELETE FROM lt_eval_projects WHERE tenant_id = ? AND id = ?`, [tenantId, id]);
2239
+ const deleted = db.getRowsModified() > 0;
2240
+ db.run("COMMIT");
2241
+ this.db.save();
2242
+ return deleted;
2243
+ } catch (error) {
2244
+ db.run("ROLLBACK");
2245
+ throw error;
2246
+ }
2173
2247
  }
2174
2248
  // -------------------------------------------------------------------------
2175
2249
  // Suites
@@ -3029,11 +3103,11 @@ var LocalA2AApiKeyStore = class {
3029
3103
  const rows = this.db.prepare(
3030
3104
  `SELECT * FROM lt_a2a_api_keys WHERE enabled = 1`
3031
3105
  ).all();
3032
- const map = /* @__PURE__ */ new Map();
3106
+ const map2 = /* @__PURE__ */ new Map();
3033
3107
  for (const row of rows) {
3034
3108
  try {
3035
3109
  const key = decrypt3(row.key_value);
3036
- map.set(key, {
3110
+ map2.set(key, {
3037
3111
  key,
3038
3112
  tenantId: row.tenant_id,
3039
3113
  projectId: row.project_id,
@@ -3042,7 +3116,7 @@ var LocalA2AApiKeyStore = class {
3042
3116
  } catch {
3043
3117
  }
3044
3118
  }
3045
- return map;
3119
+ return map2;
3046
3120
  }
3047
3121
  };
3048
3122
  function mapRowToRecord(row) {
@@ -3089,6 +3163,7 @@ CREATE INDEX IF NOT EXISTS idx_lt_tmq_pending ON lt_thread_message_queue(status,
3089
3163
  `;
3090
3164
  var LocalThreadMessageQueueStore = class {
3091
3165
  constructor(db) {
3166
+ this.capacityTail = Promise.resolve();
3092
3167
  this.db = db;
3093
3168
  ensureTable(db, DDL17);
3094
3169
  }
@@ -3107,18 +3182,65 @@ var LocalThreadMessageQueueStore = class {
3107
3182
  params.threadId,
3108
3183
  params.tenantId,
3109
3184
  params.assistantId,
3110
- params.workspaceId || null,
3111
- params.projectId || null,
3185
+ params.workspaceId ?? null,
3186
+ params.projectId ?? null,
3112
3187
  JSON.stringify(params.content),
3113
3188
  params.type || "human",
3114
3189
  nextSeq,
3115
- params.priority || 0,
3190
+ params.priority ?? 0,
3116
3191
  params.command ? JSON.stringify(params.command) : null,
3117
3192
  params.custom_run_config ? JSON.stringify(params.custom_run_config) : null,
3118
3193
  now
3119
3194
  );
3120
3195
  return this.getById(id);
3121
3196
  }
3197
+ async addMessageIfCapacity(params, maxSize) {
3198
+ let release;
3199
+ const previous = this.capacityTail;
3200
+ this.capacityTail = new Promise((resolve) => {
3201
+ release = resolve;
3202
+ });
3203
+ await previous;
3204
+ try {
3205
+ const scope = {
3206
+ tenantId: params.tenantId,
3207
+ assistantId: params.assistantId,
3208
+ workspaceId: params.workspaceId,
3209
+ projectId: params.projectId
3210
+ };
3211
+ const filter = scopeClause(scope);
3212
+ const id = params.id || randomUUID4();
3213
+ const limit = maxSize === Infinity ? -1 : maxSize;
3214
+ const result = this.db.prepare(
3215
+ `INSERT INTO lt_thread_message_queue (id, thread_id, tenant_id, assistant_id, workspace_id, project_id, message_content, message_type, sequence_order, priority, command, custom_run_config, created_at)
3216
+ SELECT ?, ?, ?, ?, ?, ?, ?, ?,
3217
+ (SELECT COALESCE(MAX(sequence_order), 0) + 1 FROM lt_thread_message_queue WHERE thread_id = ?),
3218
+ ?, ?, ?, ?
3219
+ WHERE ? = -1 OR (SELECT COUNT(*) FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'pending'${filter.sql}) < ?`
3220
+ ).run(
3221
+ id,
3222
+ params.threadId,
3223
+ params.tenantId,
3224
+ params.assistantId,
3225
+ params.workspaceId ?? null,
3226
+ params.projectId ?? null,
3227
+ JSON.stringify(params.content),
3228
+ params.type || "human",
3229
+ params.threadId,
3230
+ params.priority ?? 0,
3231
+ params.command ? JSON.stringify(params.command) : null,
3232
+ params.custom_run_config ? JSON.stringify(params.custom_run_config) : null,
3233
+ nowISO(),
3234
+ limit,
3235
+ params.threadId,
3236
+ ...filter.params,
3237
+ limit
3238
+ );
3239
+ return result.changes > 0;
3240
+ } finally {
3241
+ release();
3242
+ }
3243
+ }
3122
3244
  async addMessageAtHead(params) {
3123
3245
  const now = nowISO();
3124
3246
  const id = params.id || randomUUID4();
@@ -3133,8 +3255,8 @@ var LocalThreadMessageQueueStore = class {
3133
3255
  params.threadId,
3134
3256
  params.tenantId,
3135
3257
  params.assistantId,
3136
- params.workspaceId || null,
3137
- params.projectId || null,
3258
+ params.workspaceId ?? null,
3259
+ params.projectId ?? null,
3138
3260
  JSON.stringify(params.content),
3139
3261
  params.type || "human",
3140
3262
  seqRow.next_seq,
@@ -3144,50 +3266,63 @@ var LocalThreadMessageQueueStore = class {
3144
3266
  );
3145
3267
  return this.getById(id);
3146
3268
  }
3147
- async getPendingMessages(threadId) {
3269
+ async getPendingMessages(threadId, scope) {
3270
+ const filter = scopeClause(scope);
3148
3271
  const rows = this.db.prepare(
3149
- `SELECT * FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'pending' ORDER BY priority DESC, sequence_order ASC`
3150
- ).all(threadId);
3272
+ `SELECT * FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'pending'${filter.sql} ORDER BY priority DESC, sequence_order ASC`
3273
+ ).all(threadId, ...filter.params);
3151
3274
  return rows.map(rowToMessage);
3152
3275
  }
3153
- async getProcessingMessages(threadId) {
3276
+ async getProcessingMessages(threadId, scope) {
3277
+ const filter = scopeClause(scope);
3154
3278
  const rows = this.db.prepare(
3155
- `SELECT * FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'processing' ORDER BY priority DESC, sequence_order ASC`
3156
- ).all(threadId);
3279
+ `SELECT * FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'processing'${filter.sql} ORDER BY priority DESC, sequence_order ASC`
3280
+ ).all(threadId, ...filter.params);
3157
3281
  return rows.map(rowToMessage);
3158
3282
  }
3159
- async getQueueSize(threadId) {
3283
+ async getQueueSize(threadId, scope) {
3284
+ const filter = scopeClause(scope);
3160
3285
  const row = this.db.prepare(
3161
- `SELECT COUNT(*) as count FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'pending'`
3162
- ).get(threadId);
3286
+ `SELECT COUNT(*) as count FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'pending'${filter.sql}`
3287
+ ).get(threadId, ...filter.params);
3163
3288
  return row.count;
3164
3289
  }
3165
3290
  async getThreadsWithPendingMessages() {
3166
3291
  const rows = this.db.prepare(
3167
- `SELECT DISTINCT ON (thread_id) tenant_id, assistant_id, thread_id, workspace_id, project_id FROM lt_thread_message_queue WHERE status IN ('pending', 'processing') ORDER BY thread_id`
3292
+ `SELECT tenant_id, assistant_id, thread_id, workspace_id, project_id FROM lt_thread_message_queue WHERE status IN ('pending', 'processing') GROUP BY tenant_id, assistant_id, thread_id, workspace_id, project_id ORDER BY thread_id`
3168
3293
  ).all();
3169
3294
  return rows.map((r) => ({
3170
3295
  tenantId: r.tenant_id,
3171
3296
  assistantId: r.assistant_id,
3172
3297
  threadId: r.thread_id,
3173
- workspaceId: r.workspace_id || void 0,
3174
- projectId: r.project_id || void 0
3298
+ workspaceId: r.workspace_id,
3299
+ projectId: r.project_id
3175
3300
  }));
3176
3301
  }
3177
- async removeMessage(messageId) {
3178
- const result = this.db.prepare(`DELETE FROM lt_thread_message_queue WHERE id = ?`).run(messageId);
3302
+ async removeMessage(messageId, scope) {
3303
+ const filter = scopeClause(scope);
3304
+ const result = this.db.prepare(`DELETE FROM lt_thread_message_queue WHERE id = ?${filter.sql}`).run(messageId, ...filter.params);
3179
3305
  return result.changes > 0;
3180
3306
  }
3181
- async clearMessages(threadId) {
3182
- this.db.prepare(`DELETE FROM lt_thread_message_queue WHERE thread_id = ?`).run(threadId);
3307
+ async clearMessages(threadId, scope) {
3308
+ const filter = scopeClause(scope);
3309
+ this.db.prepare(`DELETE FROM lt_thread_message_queue WHERE thread_id = ?${filter.sql}`).run(threadId, ...filter.params);
3183
3310
  }
3184
- async markProcessing(messageId) {
3185
- this.db.prepare(`UPDATE lt_thread_message_queue SET status = 'processing' WHERE id = ?`).run(messageId);
3311
+ async markProcessing(messageId, customRunConfig, scope) {
3312
+ const filter = scopeClause(scope);
3313
+ if (customRunConfig === void 0) {
3314
+ this.db.prepare(`UPDATE lt_thread_message_queue SET status = 'processing' WHERE id = ?${filter.sql}`).run(messageId, ...filter.params);
3315
+ return;
3316
+ }
3317
+ this.db.prepare(
3318
+ `UPDATE lt_thread_message_queue SET status = 'processing', custom_run_config = ? WHERE id = ?${filter.sql}`
3319
+ ).run(JSON.stringify(customRunConfig), messageId, ...filter.params);
3186
3320
  }
3187
- async resetProcessingToPending(threadId) {
3321
+ async resetProcessingToPending(threadId, scope) {
3322
+ const filter = scopeClause(scope);
3188
3323
  const result = this.db.prepare(
3189
- `UPDATE lt_thread_message_queue SET status = 'pending' WHERE thread_id = ? AND status = 'processing'`
3190
- ).run(threadId);
3324
+ `UPDATE lt_thread_message_queue SET status = 'pending' WHERE thread_id = ? AND status = 'processing'${filter.sql}`
3325
+ ).run(threadId, ...filter.params);
3191
3326
  return result.changes;
3192
3327
  }
3193
3328
  getById(id) {
@@ -3195,6 +3330,14 @@ var LocalThreadMessageQueueStore = class {
3195
3330
  return rowToMessage(row);
3196
3331
  }
3197
3332
  };
3333
+ function scopeClause(scope) {
3334
+ if (!scope) return { sql: "", params: [] };
3335
+ const entries = ["tenantId", "assistantId", "workspaceId", "projectId"].map((key) => [key, scope[key]]);
3336
+ return {
3337
+ sql: entries.map(([key, value]) => ` AND ${key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)} ${value == null ? "IS NULL" : "= ?"}`).join(""),
3338
+ params: entries.flatMap(([, value]) => value == null ? [] : [value])
3339
+ };
3340
+ }
3198
3341
  function rowToMessage(row) {
3199
3342
  return {
3200
3343
  id: row.id,
@@ -4349,6 +4492,7 @@ var LocalTaskStore = class {
4349
4492
  };
4350
4493
 
4351
4494
  // src/stores/LocalTaskWorkItemStore.ts
4495
+ import { MAX_PENDING_EXECUTION_RESULTS_LIMIT } from "@axiom-lattice/protocols";
4352
4496
  import { v4 as uuid2 } from "uuid";
4353
4497
  var DDL21 = `
4354
4498
  CREATE TABLE IF NOT EXISTS lt_task_work_items (
@@ -4393,6 +4537,11 @@ var LocalTaskWorkItemStore = class {
4393
4537
  this.ensureColumn("lt_task_work_items", "event_key", "event_key TEXT");
4394
4538
  this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_task_work_items_event_key
4395
4539
  ON lt_task_work_items (tenant_id, task_id, event_key);`);
4540
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_task_work_items_pending_order
4541
+ ON lt_task_work_items (tenant_id, task_id, action, created_at DESC, id DESC);`);
4542
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_task_work_items_reconciled_result
4543
+ ON lt_task_work_items (tenant_id, task_id, json_extract(detail, '$.executionResultId'))
4544
+ WHERE action = 'execution_reconciled';`);
4396
4545
  }
4397
4546
  /** Add a column if it does not exist (SQLite version compatible). */
4398
4547
  ensureColumn(table, column, ddl) {
@@ -4483,6 +4632,132 @@ var LocalTaskWorkItemStore = class {
4483
4632
  ).all(...params, limit, offset);
4484
4633
  return rows.map(mapRowToWorkItem);
4485
4634
  }
4635
+ /** List pending execution results using one bounded SQLite anti-join query. */
4636
+ async listPendingExecutionResults(params) {
4637
+ if (!Number.isSafeInteger(params.limit) || params.limit < 0 || params.limit > MAX_PENDING_EXECUTION_RESULTS_LIMIT) {
4638
+ const error = new RangeError(`limit must be a safe integer between 0 and ${MAX_PENDING_EXECUTION_RESULTS_LIMIT}`);
4639
+ error.code = "INVALID_LIMIT";
4640
+ throw error;
4641
+ }
4642
+ if (params.limit === 0) return [];
4643
+ const rows = this.db.prepare(
4644
+ `SELECT result.*
4645
+ FROM lt_task_work_items AS result
4646
+ WHERE result.tenant_id = ?
4647
+ AND result.task_id = ?
4648
+ AND result.action = 'execution_result'
4649
+ AND substr(result.event_key, 1, 17) = 'execution-result:'
4650
+ AND length(result.event_key) > 17
4651
+ AND result.event_key NOT GLOB '*[^A-Za-z0-9._:-]*'
4652
+ AND NOT EXISTS (
4653
+ SELECT 1
4654
+ FROM lt_task_work_items AS reconciled
4655
+ WHERE reconciled.tenant_id = result.tenant_id
4656
+ AND reconciled.task_id = result.task_id
4657
+ AND reconciled.action = 'execution_reconciled'
4658
+ AND json_extract(reconciled.detail, '$.executionResultId') = result.event_key
4659
+ )
4660
+ ORDER BY result.created_at DESC, result.id DESC
4661
+ LIMIT ?`
4662
+ ).all(params.tenantId, params.taskId, params.limit);
4663
+ return rows.map(mapRowToWorkItem);
4664
+ }
4665
+ };
4666
+
4667
+ // src/stores/LocalCapabilityBundleStore.ts
4668
+ import { randomUUID as randomUUID6 } from "crypto";
4669
+ var DDL22 = `CREATE TABLE IF NOT EXISTS lt_capability_bundles (id TEXT NOT NULL, tenant_id TEXT NOT NULL, bundle_key TEXT NOT NULL, name TEXT NOT NULL, description TEXT, capabilities TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY (tenant_id, id), UNIQUE (tenant_id, bundle_key)); CREATE INDEX IF NOT EXISTS idx_lt_capability_bundles_tenant ON lt_capability_bundles(tenant_id);`;
4670
+ var duplicateMessage = "Capability bundle key already exists for tenant";
4671
+ function map(row) {
4672
+ return { id: row.id, tenantId: row.tenant_id, key: row.bundle_key, name: row.name, description: row.description ?? void 0, capabilities: JSON.parse(row.capabilities), createdAt: parseISO(row.created_at).toISOString(), updatedAt: parseISO(row.updated_at).toISOString() };
4673
+ }
4674
+ function isConstraint(error) {
4675
+ return error instanceof Error && /UNIQUE constraint failed/.test(error.message);
4676
+ }
4677
+ function nextTimestamp(previous) {
4678
+ const now = Date.now();
4679
+ const previousTime = previous ? Date.parse(previous) : Number.NaN;
4680
+ return new Date(Math.max(now, Number.isFinite(previousTime) ? previousTime + 1 : now)).toISOString();
4681
+ }
4682
+ var LocalCapabilityBundleStore = class {
4683
+ /**
4684
+ * Creates a capability bundle store over an initialized sql.js database.
4685
+ *
4686
+ * @param db - Shared local database wrapper used for persistence.
4687
+ */
4688
+ constructor(db) {
4689
+ this.db = db;
4690
+ ensureTable(db, DDL22);
4691
+ }
4692
+ async listByTenant(tenantId) {
4693
+ return this.db.prepare("SELECT * FROM lt_capability_bundles WHERE tenant_id = ? ORDER BY created_at").all(tenantId).map(map);
4694
+ }
4695
+ async getById(tenantId, id) {
4696
+ const row = this.db.prepare("SELECT * FROM lt_capability_bundles WHERE tenant_id = ? AND id = ?").get(tenantId, id);
4697
+ return row ? map(row) : null;
4698
+ }
4699
+ async getManyByIds(tenantId, ids) {
4700
+ return ids.flatMap((id) => {
4701
+ const row = this.db.prepare("SELECT * FROM lt_capability_bundles WHERE tenant_id = ? AND id = ?").get(tenantId, id);
4702
+ return row ? [map(row)] : [];
4703
+ });
4704
+ }
4705
+ async create(tenantId, input) {
4706
+ const now = nextTimestamp();
4707
+ const id = randomUUID6();
4708
+ try {
4709
+ this.db.prepare("INSERT INTO lt_capability_bundles (id, tenant_id, bundle_key, name, description, capabilities, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)").run(id, tenantId, input.key, input.name, input.description ?? null, JSON.stringify(input.capabilities), now, now);
4710
+ } catch (error) {
4711
+ if (isConstraint(error)) throw new Error(duplicateMessage);
4712
+ throw error;
4713
+ }
4714
+ return await this.getById(tenantId, id);
4715
+ }
4716
+ async update(tenantId, id, input) {
4717
+ const existing = await this.getById(tenantId, id);
4718
+ if (!existing) return null;
4719
+ if (input.expectedUpdatedAt !== void 0 && input.expectedUpdatedAt !== existing.updatedAt) return { status: "conflict" };
4720
+ const fields = [];
4721
+ const values = [];
4722
+ const add = (field, value) => {
4723
+ fields.push(`${field} = ?`);
4724
+ values.push(value);
4725
+ };
4726
+ if (input.name !== void 0) add("name", input.name);
4727
+ if (Object.prototype.hasOwnProperty.call(input, "description")) add("description", input.description ?? null);
4728
+ if (input.capabilities !== void 0) add("capabilities", JSON.stringify(input.capabilities));
4729
+ if (!fields.length) return existing;
4730
+ const now = nextTimestamp(existing.updatedAt);
4731
+ fields.push("updated_at = ?");
4732
+ values.push(now, tenantId, id);
4733
+ try {
4734
+ if (input.expectedUpdatedAt !== void 0) {
4735
+ const result = this.db.prepare(`UPDATE lt_capability_bundles SET ${fields.join(", ")} WHERE tenant_id = ? AND id = ? AND updated_at = ?`).run(...values, input.expectedUpdatedAt);
4736
+ if (result.changes === 0) return { status: "conflict" };
4737
+ } else {
4738
+ this.db.prepare(`UPDATE lt_capability_bundles SET ${fields.join(", ")} WHERE tenant_id = ? AND id = ?`).run(...values);
4739
+ }
4740
+ } catch (error) {
4741
+ if (isConstraint(error)) throw new Error(duplicateMessage);
4742
+ throw error;
4743
+ }
4744
+ return this.getById(tenantId, id);
4745
+ }
4746
+ async deleteIfUnreferenced(tenantId, id) {
4747
+ const result = this.db.prepare(
4748
+ `DELETE FROM lt_capability_bundles
4749
+ WHERE tenant_id = ? AND id = ?
4750
+ AND NOT EXISTS (
4751
+ SELECT 1 FROM lt_projects
4752
+ WHERE tenant_id = ?
4753
+ AND json_type(config, '$.capabilityBundleIds') = 'array'
4754
+ AND EXISTS (SELECT 1 FROM json_each(config, '$.capabilityBundleIds') WHERE value = ?)
4755
+ )`
4756
+ ).run(tenantId, id, tenantId, id);
4757
+ if (result.changes > 0) return "deleted";
4758
+ if (await this.getById(tenantId, id)) return "in_use";
4759
+ return "not_found";
4760
+ }
4486
4761
  };
4487
4762
 
4488
4763
  // src/createLocalStoreConfig.ts
@@ -4511,6 +4786,7 @@ async function createLocalStoreConfig(options = {}) {
4511
4786
  schedule: new LocalScheduleStorage(db),
4512
4787
  task: new LocalTaskStore(db),
4513
4788
  taskWorkItem: new LocalTaskWorkItemStore(db),
4789
+ capabilityBundle: new LocalCapabilityBundleStore(db),
4514
4790
  checkpoint: SqliteSaver.fromConnString(dbPath)
4515
4791
  };
4516
4792
  }
@@ -4556,6 +4832,7 @@ export {
4556
4832
  InMemoryConversationStore,
4557
4833
  LocalA2AApiKeyStore,
4558
4834
  LocalAssistantStore,
4835
+ LocalCapabilityBundleStore,
4559
4836
  LocalChannelBindingStore,
4560
4837
  LocalChannelInstallationStore,
4561
4838
  LocalConnectionStore,