@axiom-lattice/local-stores 2.0.9 → 3.0.0

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
@@ -96,8 +96,9 @@ var DatabaseWrapper = class {
96
96
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
97
  run(sql, ...params) {
98
98
  this.db.run(sql, params);
99
+ const changes = this.db.getRowsModified();
99
100
  this.save();
100
- return new RunResult(this.db);
101
+ return new RunResult(changes);
101
102
  }
102
103
  /**
103
104
  * Prepare and execute a query returning all matching rows as objects.
@@ -182,19 +183,20 @@ var StatementWrapper = class {
182
183
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
183
184
  run(...params) {
184
185
  this.db.run(this.sql, params);
186
+ const changes = this.db.getRowsModified();
185
187
  if (this.parent) {
186
188
  this.parent.save();
187
189
  }
188
- return new RunResult(this.db);
190
+ return new RunResult(changes);
189
191
  }
190
192
  };
191
193
  var RunResult = class {
192
- constructor(db) {
193
- this.db = db;
194
+ constructor(modifiedRows) {
195
+ this.modifiedRows = modifiedRows;
194
196
  }
195
197
  /** Number of rows modified by the last INSERT/UPDATE/DELETE. */
196
198
  get changes() {
197
- return this.db.getRowsModified();
199
+ return this.modifiedRows;
198
200
  }
199
201
  };
200
202
  async function initDatabase(options = {}) {
@@ -662,6 +664,7 @@ CREATE TABLE IF NOT EXISTS lt_projects (
662
664
  name TEXT NOT NULL,
663
665
  description TEXT,
664
666
  config TEXT,
667
+ kind TEXT DEFAULT 'business',
665
668
  created_at TEXT NOT NULL,
666
669
  updated_at TEXT NOT NULL,
667
670
  PRIMARY KEY (tenant_id, id)
@@ -672,8 +675,15 @@ var LocalProjectStore = class {
672
675
  constructor(db) {
673
676
  this.db = db;
674
677
  ensureTable(db, DDL4);
678
+ this.ensureColumn("lt_projects", "kind", "kind TEXT DEFAULT 'business'");
675
679
  }
676
- async getProjectsByWorkspace(tenantId, workspaceId) {
680
+ async getProjectsByWorkspace(tenantId, workspaceId, filter) {
681
+ if (filter?.kind !== void 0) {
682
+ const rows2 = this.db.prepare(
683
+ `SELECT * FROM lt_projects WHERE tenant_id = ? AND workspace_id = ? AND kind = ? ORDER BY created_at DESC`
684
+ ).all(tenantId, workspaceId, filter.kind);
685
+ return rows2.map(mapRowToProject);
686
+ }
677
687
  const rows = this.db.prepare(
678
688
  `SELECT * FROM lt_projects WHERE tenant_id = ? AND workspace_id = ? ORDER BY created_at DESC`
679
689
  ).all(tenantId, workspaceId);
@@ -687,16 +697,18 @@ var LocalProjectStore = class {
687
697
  }
688
698
  async createProject(tenantId, workspaceId, id, data) {
689
699
  const now = nowISO();
700
+ const kind = data.kind || "business";
690
701
  this.db.prepare(
691
- `INSERT INTO lt_projects (id, tenant_id, workspace_id, name, description, config, created_at, updated_at)
692
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
702
+ `INSERT INTO lt_projects (id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at)
703
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
693
704
  ON CONFLICT(tenant_id, id) DO UPDATE SET
694
705
  workspace_id = excluded.workspace_id,
695
706
  name = excluded.name,
696
707
  description = excluded.description,
697
708
  config = excluded.config,
709
+ kind = excluded.kind,
698
710
  updated_at = excluded.updated_at`
699
- ).run(id, tenantId, workspaceId, data.name, data.description || null, data.config ? JSON.stringify(data.config) : null, now, now);
711
+ ).run(id, tenantId, workspaceId, data.name, data.description || null, data.config ? JSON.stringify(data.config) : null, kind, now, now);
700
712
  return {
701
713
  id,
702
714
  tenantId,
@@ -704,6 +716,7 @@ var LocalProjectStore = class {
704
716
  name: data.name,
705
717
  description: data.description,
706
718
  config: data.config,
719
+ kind,
707
720
  createdAt: parseISO(now),
708
721
  updatedAt: parseISO(now)
709
722
  };
@@ -725,6 +738,10 @@ var LocalProjectStore = class {
725
738
  setClauses.push("config = ?");
726
739
  values.push(updates.config ? JSON.stringify(updates.config) : null);
727
740
  }
741
+ if (updates.kind !== void 0) {
742
+ setClauses.push("kind = ?");
743
+ values.push(updates.kind);
744
+ }
728
745
  if (setClauses.length === 0) return existing;
729
746
  const now = nowISO();
730
747
  setClauses.push("updated_at = ?");
@@ -741,6 +758,13 @@ var LocalProjectStore = class {
741
758
  ).run(tenantId, id);
742
759
  return result.changes > 0;
743
760
  }
761
+ /** Add a column if it does not exist (SQLite version compatible). */
762
+ ensureColumn(table, column, ddl) {
763
+ const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
764
+ if (!cols.some((c) => c.name === column)) {
765
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
766
+ }
767
+ }
744
768
  };
745
769
  function mapRowToProject(row) {
746
770
  return {
@@ -750,6 +774,7 @@ function mapRowToProject(row) {
750
774
  name: row.name,
751
775
  description: row.description || void 0,
752
776
  config: row.config ? JSON.parse(row.config) : void 0,
777
+ kind: row.kind || "business",
753
778
  createdAt: parseISO(row.created_at),
754
779
  updatedAt: parseISO(row.updated_at)
755
780
  };
@@ -2843,15 +2868,7 @@ var LocalChannelInstallationStore = class {
2843
2868
  this.db.prepare(
2844
2869
  `INSERT INTO lt_channel_installations
2845
2870
  (id, tenant_id, channel, name, config, enabled, fallback_agent_id, reject_when_no_binding, created_at, updated_at)
2846
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2847
- ON CONFLICT(id) DO UPDATE SET
2848
- channel = excluded.channel,
2849
- name = excluded.name,
2850
- config = excluded.config,
2851
- enabled = excluded.enabled,
2852
- fallback_agent_id = excluded.fallback_agent_id,
2853
- reject_when_no_binding = excluded.reject_when_no_binding,
2854
- updated_at = excluded.updated_at`
2871
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2855
2872
  ).run(
2856
2873
  installationId,
2857
2874
  tenantId,
@@ -2943,7 +2960,7 @@ CREATE TABLE IF NOT EXISTS lt_a2a_api_keys (
2943
2960
  key_value TEXT NOT NULL,
2944
2961
  tenant_id TEXT NOT NULL,
2945
2962
  project_id TEXT,
2946
- workspace_id TEXT,
2963
+ assistant_ids TEXT,
2947
2964
  label TEXT,
2948
2965
  enabled INTEGER NOT NULL DEFAULT 1,
2949
2966
  created_at TEXT NOT NULL,
@@ -2953,10 +2970,41 @@ CREATE TABLE IF NOT EXISTS lt_a2a_api_keys (
2953
2970
  function generateApiKey() {
2954
2971
  return `a2a_${(0, import_crypto3.randomUUID)().replace(/-/g, "")}`;
2955
2972
  }
2973
+ function parseAssistantIds(raw) {
2974
+ if (!raw) return void 0;
2975
+ try {
2976
+ const parsed = JSON.parse(raw);
2977
+ return Array.isArray(parsed) ? parsed : void 0;
2978
+ } catch {
2979
+ return void 0;
2980
+ }
2981
+ }
2956
2982
  var LocalA2AApiKeyStore = class {
2957
2983
  constructor(db) {
2958
2984
  this.db = db;
2959
2985
  ensureTable(db, DDL16);
2986
+ this.ensureColumn("lt_a2a_api_keys", "assistant_ids", "assistant_ids TEXT");
2987
+ this.repairManagementIds();
2988
+ }
2989
+ /** Add a column if it does not exist (SQLite version compatible). */
2990
+ ensureColumn(table, column, ddl) {
2991
+ const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
2992
+ if (!cols.some((c) => c.name === column)) {
2993
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
2994
+ }
2995
+ }
2996
+ repairManagementIds() {
2997
+ const rows = this.db.prepare(
2998
+ `SELECT rowid, id FROM lt_a2a_api_keys`
2999
+ ).all();
3000
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
3001
+ for (const row of rows) {
3002
+ if (!row.id || !uuidPattern.test(row.id)) {
3003
+ this.db.prepare(
3004
+ `UPDATE lt_a2a_api_keys SET id = ? WHERE rowid = ?`
3005
+ ).run((0, import_crypto3.randomUUID)(), row.rowid);
3006
+ }
3007
+ }
2960
3008
  }
2961
3009
  async findByKey(key) {
2962
3010
  const rows = this.db.prepare(
@@ -2970,6 +3018,12 @@ var LocalA2AApiKeyStore = class {
2970
3018
  }
2971
3019
  return null;
2972
3020
  }
3021
+ async findById(id) {
3022
+ const row = this.db.prepare(
3023
+ `SELECT * FROM lt_a2a_api_keys WHERE id = ?`
3024
+ ).get(id);
3025
+ return row ? mapRowToRecord(row) : null;
3026
+ }
2973
3027
  async list(params) {
2974
3028
  const limit = params.limit || 100;
2975
3029
  const offset = params.offset || 0;
@@ -2986,19 +3040,19 @@ var LocalA2AApiKeyStore = class {
2986
3040
  return rows.map(mapRowToRecord);
2987
3041
  }
2988
3042
  async create(input) {
3043
+ const id = (0, import_crypto3.randomUUID)();
2989
3044
  const key = generateApiKey();
2990
3045
  const now = nowISO();
2991
3046
  this.db.prepare(
2992
- `INSERT INTO lt_a2a_api_keys (key_value, tenant_id, project_id, workspace_id, label, created_at, updated_at)
2993
- VALUES (?, ?, ?, ?, ?, ?, ?)`
2994
- ).run((0, import_core3.encrypt)(key), input.tenantId, input.projectId || null, input.workspaceId || null, input.label || null, now, now);
2995
- const lastId = this.db.prepare(`SELECT last_insert_rowid() as id`).get();
3047
+ `INSERT INTO lt_a2a_api_keys (id, key_value, tenant_id, project_id, assistant_ids, label, created_at, updated_at)
3048
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
3049
+ ).run(id, (0, import_core3.encrypt)(key), input.tenantId, input.projectId, input.assistantIds ? JSON.stringify(input.assistantIds) : null, input.label || null, now, now);
2996
3050
  const record = {
2997
- id: String(lastId.id),
3051
+ id,
2998
3052
  key,
2999
3053
  tenantId: input.tenantId,
3000
3054
  projectId: input.projectId,
3001
- workspaceId: input.workspaceId,
3055
+ assistantIds: input.assistantIds,
3002
3056
  label: input.label,
3003
3057
  enabled: true,
3004
3058
  createdAt: parseISO(now),
@@ -3050,8 +3104,8 @@ var LocalA2AApiKeyStore = class {
3050
3104
  map.set(key, {
3051
3105
  key,
3052
3106
  tenantId: row.tenant_id,
3053
- projectId: row.project_id || void 0,
3054
- workspaceId: row.workspace_id || void 0
3107
+ projectId: row.project_id,
3108
+ assistantIds: parseAssistantIds(row.assistant_ids)
3055
3109
  });
3056
3110
  } catch {
3057
3111
  }
@@ -3070,8 +3124,8 @@ function mapRowToRecord(row) {
3070
3124
  id: row.id,
3071
3125
  key,
3072
3126
  tenantId: row.tenant_id,
3073
- projectId: row.project_id || void 0,
3074
- workspaceId: row.workspace_id || void 0,
3127
+ projectId: row.project_id,
3128
+ assistantIds: parseAssistantIds(row.assistant_ids),
3075
3129
  label: row.label || void 0,
3076
3130
  enabled: row.enabled === 1,
3077
3131
  createdAt: parseISO(row.created_at),
@@ -3720,6 +3774,12 @@ function mapRowToTask(row) {
3720
3774
 
3721
3775
  // src/stores/LocalTaskStore.ts
3722
3776
  var import_crypto5 = require("crypto");
3777
+ var MAX_CANONICAL_TASK_TIME = "9999-12-31T23:59:59.999Z";
3778
+ var NEXT_UPDATED_AT_SQL = `updated_at = CASE
3779
+ WHEN strftime('%Y-%m-%dT%H:%M:%fZ', updated_at) = updated_at AND updated_at >= ?
3780
+ THEN strftime('%Y-%m-%dT%H:%M:%fZ', updated_at, '+0.001 seconds')
3781
+ ELSE ?
3782
+ END`;
3723
3783
  var DDL20 = `
3724
3784
  CREATE TABLE IF NOT EXISTS lt_tasks (
3725
3785
  id TEXT NOT NULL,
@@ -3783,6 +3843,10 @@ function mapRowToTask2(row) {
3783
3843
  updatedAt: parseISO(row.updated_at)
3784
3844
  };
3785
3845
  }
3846
+ function nextISO(previous) {
3847
+ const previousMs = new Date(previous).getTime();
3848
+ return new Date(Math.max(Date.now(), previousMs + 1)).toISOString();
3849
+ }
3786
3850
  var LocalTaskStore = class {
3787
3851
  constructor(db) {
3788
3852
  this.db = db;
@@ -3803,7 +3867,7 @@ var LocalTaskStore = class {
3803
3867
  }
3804
3868
  }
3805
3869
  async create(params) {
3806
- const id = (0, import_crypto5.randomUUID)();
3870
+ const id = params.id ?? (0, import_crypto5.randomUUID)();
3807
3871
  const now = nowISO();
3808
3872
  this.db.prepare(
3809
3873
  `INSERT INTO lt_tasks (id, tenant_id, owner_type, owner_id, title, description, status, priority, due_date, metadata, parent_id, source_id, context, require_review, dependencies, result, failure_reason, workspace_id, project_id, files, created_at, updated_at)
@@ -3904,8 +3968,8 @@ var LocalTaskStore = class {
3904
3968
  const existing = await this.getById(tenantId, id);
3905
3969
  if (!existing) return null;
3906
3970
  const now = nowISO();
3907
- const setClauses = ["updated_at = ?"];
3908
- const values = [now];
3971
+ const setClauses = [NEXT_UPDATED_AT_SQL];
3972
+ const values = [now, now];
3909
3973
  if (updates.title !== void 0) {
3910
3974
  setClauses.push("title = ?");
3911
3975
  values.push(updates.title);
@@ -3940,7 +4004,7 @@ var LocalTaskStore = class {
3940
4004
  }
3941
4005
  if (updates.context !== void 0) {
3942
4006
  setClauses.push("context = ?");
3943
- values.push(JSON.stringify(updates.context));
4007
+ values.push(updates.context === null ? null : JSON.stringify(updates.context));
3944
4008
  }
3945
4009
  if (updates.ownerType !== void 0) {
3946
4010
  setClauses.push("owner_type = ?");
@@ -3979,10 +4043,369 @@ var LocalTaskStore = class {
3979
4043
  values.push(JSON.stringify(updates.files));
3980
4044
  }
3981
4045
  if (setClauses.length === 1) return existing;
3982
- values.push(tenantId, id);
3983
- this.db.prepare(
3984
- `UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ?`
4046
+ values.push(tenantId, id, MAX_CANONICAL_TASK_TIME);
4047
+ const result = this.db.prepare(
4048
+ `UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ? AND updated_at <> ?`
4049
+ ).run(...values);
4050
+ if (result.changes === 0) return null;
4051
+ return this.getById(tenantId, id);
4052
+ }
4053
+ /**
4054
+ * Atomically update a task unless its current status is blocked.
4055
+ *
4056
+ * @param tenantId Tenant identifier.
4057
+ * @param id Task identifier.
4058
+ * @param updates Partial task data to update.
4059
+ * @param blockedStatuses Current statuses that prevent the update.
4060
+ * @returns The updated task, or `null` when missing or blocked.
4061
+ */
4062
+ async updateIfStatusNotIn(tenantId, id, updates, blockedStatuses) {
4063
+ const now = nowISO();
4064
+ const setClauses = [NEXT_UPDATED_AT_SQL];
4065
+ const values = [now, now];
4066
+ if (updates.title !== void 0) {
4067
+ setClauses.push("title = ?");
4068
+ values.push(updates.title);
4069
+ }
4070
+ if (updates.description !== void 0) {
4071
+ setClauses.push("description = ?");
4072
+ values.push(updates.description);
4073
+ }
4074
+ if (updates.status !== void 0) {
4075
+ setClauses.push("status = ?");
4076
+ values.push(updates.status);
4077
+ }
4078
+ if (updates.priority !== void 0) {
4079
+ setClauses.push("priority = ?");
4080
+ values.push(updates.priority);
4081
+ }
4082
+ if (updates.dueDate !== void 0) {
4083
+ setClauses.push("due_date = ?");
4084
+ values.push(updates.dueDate);
4085
+ }
4086
+ if (updates.metadata !== void 0) {
4087
+ setClauses.push("metadata = ?");
4088
+ values.push(JSON.stringify(updates.metadata));
4089
+ }
4090
+ if (updates.parentId !== void 0) {
4091
+ setClauses.push("parent_id = ?");
4092
+ values.push(updates.parentId);
4093
+ }
4094
+ if (updates.sourceId !== void 0) {
4095
+ setClauses.push("source_id = ?");
4096
+ values.push(updates.sourceId);
4097
+ }
4098
+ if (updates.context !== void 0) {
4099
+ setClauses.push("context = ?");
4100
+ values.push(updates.context === null ? null : JSON.stringify(updates.context));
4101
+ }
4102
+ if (updates.ownerType !== void 0) {
4103
+ setClauses.push("owner_type = ?");
4104
+ values.push(updates.ownerType);
4105
+ }
4106
+ if (updates.ownerId !== void 0) {
4107
+ setClauses.push("owner_id = ?");
4108
+ values.push(updates.ownerId);
4109
+ }
4110
+ if (updates.requireReview !== void 0) {
4111
+ setClauses.push("require_review = ?");
4112
+ values.push(updates.requireReview ? 1 : 0);
4113
+ }
4114
+ if (updates.dependencies !== void 0) {
4115
+ setClauses.push("dependencies = ?");
4116
+ values.push(JSON.stringify(updates.dependencies));
4117
+ }
4118
+ if (updates.result !== void 0) {
4119
+ setClauses.push("result = ?");
4120
+ values.push(updates.result);
4121
+ }
4122
+ if (updates.failureReason !== void 0) {
4123
+ setClauses.push("failure_reason = ?");
4124
+ values.push(updates.failureReason);
4125
+ }
4126
+ if (updates.workspaceId !== void 0) {
4127
+ setClauses.push("workspace_id = ?");
4128
+ values.push(updates.workspaceId);
4129
+ }
4130
+ if (updates.projectId !== void 0) {
4131
+ setClauses.push("project_id = ?");
4132
+ values.push(updates.projectId);
4133
+ }
4134
+ if (updates.files !== void 0) {
4135
+ setClauses.push("files = ?");
4136
+ values.push(JSON.stringify(updates.files));
4137
+ }
4138
+ const statusClause = blockedStatuses.length > 0 ? ` AND status NOT IN (${blockedStatuses.map(() => "?").join(", ")})` : "";
4139
+ values.push(tenantId, id, MAX_CANONICAL_TASK_TIME, ...blockedStatuses);
4140
+ const result = this.db.prepare(
4141
+ `UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ? AND updated_at <> ?${statusClause}`
4142
+ ).run(...values);
4143
+ if (result.changes === 0) return null;
4144
+ return this.getById(tenantId, id);
4145
+ }
4146
+ /** Atomically update a task only when its current status is expected. */
4147
+ async updateIfStatusIn(tenantId, id, updates, expectedStatuses) {
4148
+ if (expectedStatuses.length === 0) return null;
4149
+ const now = nowISO();
4150
+ const setClauses = [NEXT_UPDATED_AT_SQL];
4151
+ const values = [now, now];
4152
+ if (updates.title !== void 0) {
4153
+ setClauses.push("title = ?");
4154
+ values.push(updates.title);
4155
+ }
4156
+ if (updates.description !== void 0) {
4157
+ setClauses.push("description = ?");
4158
+ values.push(updates.description);
4159
+ }
4160
+ if (updates.status !== void 0) {
4161
+ setClauses.push("status = ?");
4162
+ values.push(updates.status);
4163
+ }
4164
+ if (updates.priority !== void 0) {
4165
+ setClauses.push("priority = ?");
4166
+ values.push(updates.priority);
4167
+ }
4168
+ if (updates.dueDate !== void 0) {
4169
+ setClauses.push("due_date = ?");
4170
+ values.push(updates.dueDate);
4171
+ }
4172
+ if (updates.metadata !== void 0) {
4173
+ setClauses.push("metadata = ?");
4174
+ values.push(JSON.stringify(updates.metadata));
4175
+ }
4176
+ if (updates.parentId !== void 0) {
4177
+ setClauses.push("parent_id = ?");
4178
+ values.push(updates.parentId);
4179
+ }
4180
+ if (updates.sourceId !== void 0) {
4181
+ setClauses.push("source_id = ?");
4182
+ values.push(updates.sourceId);
4183
+ }
4184
+ if (updates.context !== void 0) {
4185
+ setClauses.push("context = ?");
4186
+ values.push(updates.context === null ? null : JSON.stringify(updates.context));
4187
+ }
4188
+ if (updates.ownerType !== void 0) {
4189
+ setClauses.push("owner_type = ?");
4190
+ values.push(updates.ownerType);
4191
+ }
4192
+ if (updates.ownerId !== void 0) {
4193
+ setClauses.push("owner_id = ?");
4194
+ values.push(updates.ownerId);
4195
+ }
4196
+ if (updates.requireReview !== void 0) {
4197
+ setClauses.push("require_review = ?");
4198
+ values.push(updates.requireReview ? 1 : 0);
4199
+ }
4200
+ if (updates.dependencies !== void 0) {
4201
+ setClauses.push("dependencies = ?");
4202
+ values.push(JSON.stringify(updates.dependencies));
4203
+ }
4204
+ if (updates.result !== void 0) {
4205
+ setClauses.push("result = ?");
4206
+ values.push(updates.result);
4207
+ }
4208
+ if (updates.failureReason !== void 0) {
4209
+ setClauses.push("failure_reason = ?");
4210
+ values.push(updates.failureReason);
4211
+ }
4212
+ if (updates.workspaceId !== void 0) {
4213
+ setClauses.push("workspace_id = ?");
4214
+ values.push(updates.workspaceId);
4215
+ }
4216
+ if (updates.projectId !== void 0) {
4217
+ setClauses.push("project_id = ?");
4218
+ values.push(updates.projectId);
4219
+ }
4220
+ if (updates.files !== void 0) {
4221
+ setClauses.push("files = ?");
4222
+ values.push(JSON.stringify(updates.files));
4223
+ }
4224
+ values.push(tenantId, id, MAX_CANONICAL_TASK_TIME, ...expectedStatuses);
4225
+ const result = this.db.prepare(
4226
+ `UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ? AND updated_at <> ? AND status IN (${expectedStatuses.map(() => "?").join(", ")})`
4227
+ ).run(...values);
4228
+ if (result.changes === 0) return null;
4229
+ return this.getById(tenantId, id);
4230
+ }
4231
+ /** Atomically update a task only when status and updatedAt match a read snapshot. */
4232
+ async updateIfStatusAndUpdatedAt(tenantId, id, updates, expectedStatuses, expectedUpdatedAt) {
4233
+ if (expectedStatuses.length === 0) return null;
4234
+ const expectedIso = new Date(expectedUpdatedAt).toISOString();
4235
+ if (expectedIso === MAX_CANONICAL_TASK_TIME) return null;
4236
+ const setClauses = ["updated_at = ?"];
4237
+ const values = [nextISO(expectedIso)];
4238
+ if (updates.title !== void 0) {
4239
+ setClauses.push("title = ?");
4240
+ values.push(updates.title);
4241
+ }
4242
+ if (updates.description !== void 0) {
4243
+ setClauses.push("description = ?");
4244
+ values.push(updates.description);
4245
+ }
4246
+ if (updates.status !== void 0) {
4247
+ setClauses.push("status = ?");
4248
+ values.push(updates.status);
4249
+ }
4250
+ if (updates.priority !== void 0) {
4251
+ setClauses.push("priority = ?");
4252
+ values.push(updates.priority);
4253
+ }
4254
+ if (updates.dueDate !== void 0) {
4255
+ setClauses.push("due_date = ?");
4256
+ values.push(updates.dueDate);
4257
+ }
4258
+ if (updates.metadata !== void 0) {
4259
+ setClauses.push("metadata = ?");
4260
+ values.push(JSON.stringify(updates.metadata));
4261
+ }
4262
+ if (updates.parentId !== void 0) {
4263
+ setClauses.push("parent_id = ?");
4264
+ values.push(updates.parentId);
4265
+ }
4266
+ if (updates.sourceId !== void 0) {
4267
+ setClauses.push("source_id = ?");
4268
+ values.push(updates.sourceId);
4269
+ }
4270
+ if (updates.context !== void 0) {
4271
+ setClauses.push("context = ?");
4272
+ values.push(updates.context === null ? null : JSON.stringify(updates.context));
4273
+ }
4274
+ if (updates.ownerType !== void 0) {
4275
+ setClauses.push("owner_type = ?");
4276
+ values.push(updates.ownerType);
4277
+ }
4278
+ if (updates.ownerId !== void 0) {
4279
+ setClauses.push("owner_id = ?");
4280
+ values.push(updates.ownerId);
4281
+ }
4282
+ if (updates.requireReview !== void 0) {
4283
+ setClauses.push("require_review = ?");
4284
+ values.push(updates.requireReview ? 1 : 0);
4285
+ }
4286
+ if (updates.dependencies !== void 0) {
4287
+ setClauses.push("dependencies = ?");
4288
+ values.push(JSON.stringify(updates.dependencies));
4289
+ }
4290
+ if (updates.result !== void 0) {
4291
+ setClauses.push("result = ?");
4292
+ values.push(updates.result);
4293
+ }
4294
+ if (updates.failureReason !== void 0) {
4295
+ setClauses.push("failure_reason = ?");
4296
+ values.push(updates.failureReason);
4297
+ }
4298
+ if (updates.workspaceId !== void 0) {
4299
+ setClauses.push("workspace_id = ?");
4300
+ values.push(updates.workspaceId);
4301
+ }
4302
+ if (updates.projectId !== void 0) {
4303
+ setClauses.push("project_id = ?");
4304
+ values.push(updates.projectId);
4305
+ }
4306
+ if (updates.files !== void 0) {
4307
+ setClauses.push("files = ?");
4308
+ values.push(JSON.stringify(updates.files));
4309
+ }
4310
+ values.push(tenantId, id, ...expectedStatuses, expectedIso);
4311
+ const result = this.db.prepare(
4312
+ `UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ? AND status IN (${expectedStatuses.map(() => "?").join(", ")}) AND updated_at = ?`
4313
+ ).run(...values);
4314
+ if (result.changes === 0) return null;
4315
+ return this.getById(tenantId, id);
4316
+ }
4317
+ /** Atomically update a child only when both child and parent snapshots match. */
4318
+ async updateIfStatusUpdatedAtAndParentUpdatedAt(tenantId, id, updates, expectedStatuses, expectedUpdatedAt, parentId, expectedParentUpdatedAt) {
4319
+ if (expectedStatuses.length === 0) return null;
4320
+ const expectedIso = new Date(expectedUpdatedAt).toISOString();
4321
+ if (expectedIso === MAX_CANONICAL_TASK_TIME) return null;
4322
+ const parentExpectedIso = new Date(expectedParentUpdatedAt).toISOString();
4323
+ const setClauses = ["updated_at = ?"];
4324
+ const values = [nextISO(expectedIso)];
4325
+ if (updates.title !== void 0) {
4326
+ setClauses.push("title = ?");
4327
+ values.push(updates.title);
4328
+ }
4329
+ if (updates.description !== void 0) {
4330
+ setClauses.push("description = ?");
4331
+ values.push(updates.description);
4332
+ }
4333
+ if (updates.status !== void 0) {
4334
+ setClauses.push("status = ?");
4335
+ values.push(updates.status);
4336
+ }
4337
+ if (updates.priority !== void 0) {
4338
+ setClauses.push("priority = ?");
4339
+ values.push(updates.priority);
4340
+ }
4341
+ if (updates.dueDate !== void 0) {
4342
+ setClauses.push("due_date = ?");
4343
+ values.push(updates.dueDate);
4344
+ }
4345
+ if (updates.metadata !== void 0) {
4346
+ setClauses.push("metadata = ?");
4347
+ values.push(JSON.stringify(updates.metadata));
4348
+ }
4349
+ if (updates.parentId !== void 0) {
4350
+ setClauses.push("parent_id = ?");
4351
+ values.push(updates.parentId);
4352
+ }
4353
+ if (updates.sourceId !== void 0) {
4354
+ setClauses.push("source_id = ?");
4355
+ values.push(updates.sourceId);
4356
+ }
4357
+ if (updates.context !== void 0) {
4358
+ setClauses.push("context = ?");
4359
+ values.push(updates.context === null ? null : JSON.stringify(updates.context));
4360
+ }
4361
+ if (updates.ownerType !== void 0) {
4362
+ setClauses.push("owner_type = ?");
4363
+ values.push(updates.ownerType);
4364
+ }
4365
+ if (updates.ownerId !== void 0) {
4366
+ setClauses.push("owner_id = ?");
4367
+ values.push(updates.ownerId);
4368
+ }
4369
+ if (updates.requireReview !== void 0) {
4370
+ setClauses.push("require_review = ?");
4371
+ values.push(updates.requireReview ? 1 : 0);
4372
+ }
4373
+ if (updates.dependencies !== void 0) {
4374
+ setClauses.push("dependencies = ?");
4375
+ values.push(JSON.stringify(updates.dependencies));
4376
+ }
4377
+ if (updates.result !== void 0) {
4378
+ setClauses.push("result = ?");
4379
+ values.push(updates.result);
4380
+ }
4381
+ if (updates.failureReason !== void 0) {
4382
+ setClauses.push("failure_reason = ?");
4383
+ values.push(updates.failureReason);
4384
+ }
4385
+ if (updates.workspaceId !== void 0) {
4386
+ setClauses.push("workspace_id = ?");
4387
+ values.push(updates.workspaceId);
4388
+ }
4389
+ if (updates.projectId !== void 0) {
4390
+ setClauses.push("project_id = ?");
4391
+ values.push(updates.projectId);
4392
+ }
4393
+ if (updates.files !== void 0) {
4394
+ setClauses.push("files = ?");
4395
+ values.push(JSON.stringify(updates.files));
4396
+ }
4397
+ values.push(tenantId, id, ...expectedStatuses, expectedIso, parentId, parentExpectedIso);
4398
+ const result = this.db.prepare(
4399
+ `UPDATE lt_tasks AS child SET ${setClauses.join(", ")}
4400
+ WHERE tenant_id = ? AND id = ?
4401
+ AND status IN (${expectedStatuses.map(() => "?").join(", ")})
4402
+ AND updated_at = ?
4403
+ AND EXISTS (
4404
+ SELECT 1 FROM lt_tasks AS parent
4405
+ WHERE parent.tenant_id = child.tenant_id AND parent.id = ? AND parent.updated_at = ?
4406
+ )`
3985
4407
  ).run(...values);
4408
+ if (result.changes === 0) return null;
3986
4409
  return this.getById(tenantId, id);
3987
4410
  }
3988
4411
  async delete(tenantId, id) {
@@ -4025,6 +4448,7 @@ function mapRowToWorkItem(row) {
4025
4448
  attempt: row.attempt ?? void 0,
4026
4449
  workspaceId: row.workspace_id ?? void 0,
4027
4450
  projectId: row.project_id ?? void 0,
4451
+ eventKey: row.event_key ?? void 0,
4028
4452
  createdAt: parseISO(row.created_at)
4029
4453
  };
4030
4454
  }
@@ -4032,8 +4456,18 @@ var LocalTaskWorkItemStore = class {
4032
4456
  constructor(db) {
4033
4457
  this.db = db;
4034
4458
  ensureTable(db, DDL21);
4035
- this.db.exec(`ALTER TABLE lt_task_work_items ADD COLUMN workspace_id TEXT;`);
4036
- this.db.exec(`ALTER TABLE lt_task_work_items ADD COLUMN project_id TEXT;`);
4459
+ this.ensureColumn("lt_task_work_items", "workspace_id", "workspace_id TEXT");
4460
+ this.ensureColumn("lt_task_work_items", "project_id", "project_id TEXT");
4461
+ this.ensureColumn("lt_task_work_items", "event_key", "event_key TEXT");
4462
+ this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_task_work_items_event_key
4463
+ ON lt_task_work_items (tenant_id, task_id, event_key);`);
4464
+ }
4465
+ /** Add a column if it does not exist (SQLite version compatible). */
4466
+ ensureColumn(table, column, ddl) {
4467
+ const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
4468
+ if (!cols.some((c) => c.name === column)) {
4469
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
4470
+ }
4037
4471
  }
4038
4472
  async create(params) {
4039
4473
  const id = (0, import_uuid2.v4)();
@@ -4060,6 +4494,39 @@ var LocalTaskWorkItemStore = class {
4060
4494
  ).get(params.tenantId, id);
4061
4495
  return mapRowToWorkItem(row);
4062
4496
  }
4497
+ /** Find an event by its tenant- and task-scoped key without pagination. */
4498
+ async findByEventKey(tenantId, taskId, eventKey) {
4499
+ const row = this.db.prepare(
4500
+ `SELECT * FROM lt_task_work_items WHERE tenant_id = ? AND task_id = ? AND event_key = ?`
4501
+ ).get(tenantId, taskId, eventKey);
4502
+ return row ? mapRowToWorkItem(row) : null;
4503
+ }
4504
+ /** Atomically return an existing event or create it once. */
4505
+ async createIfAbsentByEventKey(params) {
4506
+ const id = (0, import_uuid2.v4)();
4507
+ this.db.prepare(
4508
+ `INSERT INTO lt_task_work_items (id, tenant_id, task_id, action, actor, thread_id, summary, detail, attempt, workspace_id, project_id, event_key, created_at)
4509
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4510
+ ON CONFLICT (tenant_id, task_id, event_key) DO NOTHING`
4511
+ ).run(
4512
+ id,
4513
+ params.tenantId,
4514
+ params.taskId,
4515
+ params.action,
4516
+ params.actor,
4517
+ params.threadId || null,
4518
+ params.summary || null,
4519
+ params.detail ? JSON.stringify(params.detail) : null,
4520
+ params.attempt ?? null,
4521
+ params.workspaceId || null,
4522
+ params.projectId || null,
4523
+ params.eventKey,
4524
+ nowISO()
4525
+ );
4526
+ const item = await this.findByEventKey(params.tenantId, params.taskId, params.eventKey);
4527
+ if (!item) throw new Error("Task work item insert did not return a row");
4528
+ return item;
4529
+ }
4063
4530
  async list(filter) {
4064
4531
  const conditions = ["tenant_id = ?", "task_id = ?"];
4065
4532
  const params = [filter.tenantId, filter.taskId];
@@ -4067,11 +4534,20 @@ var LocalTaskWorkItemStore = class {
4067
4534
  conditions.push("action = ?");
4068
4535
  params.push(filter.action);
4069
4536
  }
4537
+ if (filter.workspaceId) {
4538
+ conditions.push("workspace_id = ?");
4539
+ params.push(filter.workspaceId);
4540
+ }
4541
+ if (filter.projectId) {
4542
+ conditions.push("project_id = ?");
4543
+ params.push(filter.projectId);
4544
+ }
4070
4545
  const where = conditions.join(" AND ");
4071
4546
  const limit = filter.limit || 100;
4072
4547
  const offset = filter.offset || 0;
4548
+ const order = filter.order === "asc" ? "ASC" : "DESC";
4073
4549
  const rows = this.db.prepare(
4074
- `SELECT * FROM lt_task_work_items WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`
4550
+ `SELECT * FROM lt_task_work_items WHERE ${where} ORDER BY created_at ${order}, id ${order} LIMIT ? OFFSET ?`
4075
4551
  ).all(...params, limit, offset);
4076
4552
  return rows.map(mapRowToWorkItem);
4077
4553
  }