@axiom-lattice/local-stores 2.0.10 → 3.0.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
@@ -28,8 +28,9 @@ var DatabaseWrapper = class {
28
28
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
29
  run(sql, ...params) {
30
30
  this.db.run(sql, params);
31
+ const changes = this.db.getRowsModified();
31
32
  this.save();
32
- return new RunResult(this.db);
33
+ return new RunResult(changes);
33
34
  }
34
35
  /**
35
36
  * Prepare and execute a query returning all matching rows as objects.
@@ -114,19 +115,20 @@ var StatementWrapper = class {
114
115
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
115
116
  run(...params) {
116
117
  this.db.run(this.sql, params);
118
+ const changes = this.db.getRowsModified();
117
119
  if (this.parent) {
118
120
  this.parent.save();
119
121
  }
120
- return new RunResult(this.db);
122
+ return new RunResult(changes);
121
123
  }
122
124
  };
123
125
  var RunResult = class {
124
- constructor(db) {
125
- this.db = db;
126
+ constructor(modifiedRows) {
127
+ this.modifiedRows = modifiedRows;
126
128
  }
127
129
  /** Number of rows modified by the last INSERT/UPDATE/DELETE. */
128
130
  get changes() {
129
- return this.db.getRowsModified();
131
+ return this.modifiedRows;
130
132
  }
131
133
  };
132
134
  async function initDatabase(options = {}) {
@@ -594,6 +596,7 @@ CREATE TABLE IF NOT EXISTS lt_projects (
594
596
  name TEXT NOT NULL,
595
597
  description TEXT,
596
598
  config TEXT,
599
+ kind TEXT DEFAULT 'business',
597
600
  created_at TEXT NOT NULL,
598
601
  updated_at TEXT NOT NULL,
599
602
  PRIMARY KEY (tenant_id, id)
@@ -604,8 +607,15 @@ var LocalProjectStore = class {
604
607
  constructor(db) {
605
608
  this.db = db;
606
609
  ensureTable(db, DDL4);
610
+ this.ensureColumn("lt_projects", "kind", "kind TEXT DEFAULT 'business'");
607
611
  }
608
- async getProjectsByWorkspace(tenantId, workspaceId) {
612
+ async getProjectsByWorkspace(tenantId, workspaceId, filter) {
613
+ if (filter?.kind !== void 0) {
614
+ const rows2 = this.db.prepare(
615
+ `SELECT * FROM lt_projects WHERE tenant_id = ? AND workspace_id = ? AND kind = ? ORDER BY created_at DESC`
616
+ ).all(tenantId, workspaceId, filter.kind);
617
+ return rows2.map(mapRowToProject);
618
+ }
609
619
  const rows = this.db.prepare(
610
620
  `SELECT * FROM lt_projects WHERE tenant_id = ? AND workspace_id = ? ORDER BY created_at DESC`
611
621
  ).all(tenantId, workspaceId);
@@ -619,16 +629,18 @@ var LocalProjectStore = class {
619
629
  }
620
630
  async createProject(tenantId, workspaceId, id, data) {
621
631
  const now = nowISO();
632
+ const kind = data.kind || "business";
622
633
  this.db.prepare(
623
- `INSERT INTO lt_projects (id, tenant_id, workspace_id, name, description, config, created_at, updated_at)
624
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
634
+ `INSERT INTO lt_projects (id, tenant_id, workspace_id, name, description, config, kind, created_at, updated_at)
635
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
625
636
  ON CONFLICT(tenant_id, id) DO UPDATE SET
626
637
  workspace_id = excluded.workspace_id,
627
638
  name = excluded.name,
628
639
  description = excluded.description,
629
640
  config = excluded.config,
641
+ kind = excluded.kind,
630
642
  updated_at = excluded.updated_at`
631
- ).run(id, tenantId, workspaceId, data.name, data.description || null, data.config ? JSON.stringify(data.config) : null, now, now);
643
+ ).run(id, tenantId, workspaceId, data.name, data.description || null, data.config ? JSON.stringify(data.config) : null, kind, now, now);
632
644
  return {
633
645
  id,
634
646
  tenantId,
@@ -636,6 +648,7 @@ var LocalProjectStore = class {
636
648
  name: data.name,
637
649
  description: data.description,
638
650
  config: data.config,
651
+ kind,
639
652
  createdAt: parseISO(now),
640
653
  updatedAt: parseISO(now)
641
654
  };
@@ -657,6 +670,10 @@ var LocalProjectStore = class {
657
670
  setClauses.push("config = ?");
658
671
  values.push(updates.config ? JSON.stringify(updates.config) : null);
659
672
  }
673
+ if (updates.kind !== void 0) {
674
+ setClauses.push("kind = ?");
675
+ values.push(updates.kind);
676
+ }
660
677
  if (setClauses.length === 0) return existing;
661
678
  const now = nowISO();
662
679
  setClauses.push("updated_at = ?");
@@ -673,6 +690,13 @@ var LocalProjectStore = class {
673
690
  ).run(tenantId, id);
674
691
  return result.changes > 0;
675
692
  }
693
+ /** Add a column if it does not exist (SQLite version compatible). */
694
+ ensureColumn(table, column, ddl) {
695
+ const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
696
+ if (!cols.some((c) => c.name === column)) {
697
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
698
+ }
699
+ }
676
700
  };
677
701
  function mapRowToProject(row) {
678
702
  return {
@@ -682,6 +706,7 @@ function mapRowToProject(row) {
682
706
  name: row.name,
683
707
  description: row.description || void 0,
684
708
  config: row.config ? JSON.parse(row.config) : void 0,
709
+ kind: row.kind || "business",
685
710
  createdAt: parseISO(row.created_at),
686
711
  updatedAt: parseISO(row.updated_at)
687
712
  };
@@ -2775,15 +2800,7 @@ var LocalChannelInstallationStore = class {
2775
2800
  this.db.prepare(
2776
2801
  `INSERT INTO lt_channel_installations
2777
2802
  (id, tenant_id, channel, name, config, enabled, fallback_agent_id, reject_when_no_binding, created_at, updated_at)
2778
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2779
- ON CONFLICT(id) DO UPDATE SET
2780
- channel = excluded.channel,
2781
- name = excluded.name,
2782
- config = excluded.config,
2783
- enabled = excluded.enabled,
2784
- fallback_agent_id = excluded.fallback_agent_id,
2785
- reject_when_no_binding = excluded.reject_when_no_binding,
2786
- updated_at = excluded.updated_at`
2803
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
2787
2804
  ).run(
2788
2805
  installationId,
2789
2806
  tenantId,
@@ -2875,7 +2892,7 @@ CREATE TABLE IF NOT EXISTS lt_a2a_api_keys (
2875
2892
  key_value TEXT NOT NULL,
2876
2893
  tenant_id TEXT NOT NULL,
2877
2894
  project_id TEXT,
2878
- workspace_id TEXT,
2895
+ assistant_ids TEXT,
2879
2896
  label TEXT,
2880
2897
  enabled INTEGER NOT NULL DEFAULT 1,
2881
2898
  created_at TEXT NOT NULL,
@@ -2885,10 +2902,41 @@ CREATE TABLE IF NOT EXISTS lt_a2a_api_keys (
2885
2902
  function generateApiKey() {
2886
2903
  return `a2a_${randomUUID3().replace(/-/g, "")}`;
2887
2904
  }
2905
+ function parseAssistantIds(raw) {
2906
+ if (!raw) return void 0;
2907
+ try {
2908
+ const parsed = JSON.parse(raw);
2909
+ return Array.isArray(parsed) ? parsed : void 0;
2910
+ } catch {
2911
+ return void 0;
2912
+ }
2913
+ }
2888
2914
  var LocalA2AApiKeyStore = class {
2889
2915
  constructor(db) {
2890
2916
  this.db = db;
2891
2917
  ensureTable(db, DDL16);
2918
+ this.ensureColumn("lt_a2a_api_keys", "assistant_ids", "assistant_ids TEXT");
2919
+ this.repairManagementIds();
2920
+ }
2921
+ /** Add a column if it does not exist (SQLite version compatible). */
2922
+ ensureColumn(table, column, ddl) {
2923
+ const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
2924
+ if (!cols.some((c) => c.name === column)) {
2925
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
2926
+ }
2927
+ }
2928
+ repairManagementIds() {
2929
+ const rows = this.db.prepare(
2930
+ `SELECT rowid, id FROM lt_a2a_api_keys`
2931
+ ).all();
2932
+ 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;
2933
+ for (const row of rows) {
2934
+ if (!row.id || !uuidPattern.test(row.id)) {
2935
+ this.db.prepare(
2936
+ `UPDATE lt_a2a_api_keys SET id = ? WHERE rowid = ?`
2937
+ ).run(randomUUID3(), row.rowid);
2938
+ }
2939
+ }
2892
2940
  }
2893
2941
  async findByKey(key) {
2894
2942
  const rows = this.db.prepare(
@@ -2902,6 +2950,12 @@ var LocalA2AApiKeyStore = class {
2902
2950
  }
2903
2951
  return null;
2904
2952
  }
2953
+ async findById(id) {
2954
+ const row = this.db.prepare(
2955
+ `SELECT * FROM lt_a2a_api_keys WHERE id = ?`
2956
+ ).get(id);
2957
+ return row ? mapRowToRecord(row) : null;
2958
+ }
2905
2959
  async list(params) {
2906
2960
  const limit = params.limit || 100;
2907
2961
  const offset = params.offset || 0;
@@ -2918,19 +2972,19 @@ var LocalA2AApiKeyStore = class {
2918
2972
  return rows.map(mapRowToRecord);
2919
2973
  }
2920
2974
  async create(input) {
2975
+ const id = randomUUID3();
2921
2976
  const key = generateApiKey();
2922
2977
  const now = nowISO();
2923
2978
  this.db.prepare(
2924
- `INSERT INTO lt_a2a_api_keys (key_value, tenant_id, project_id, workspace_id, label, created_at, updated_at)
2925
- VALUES (?, ?, ?, ?, ?, ?, ?)`
2926
- ).run(encrypt3(key), input.tenantId, input.projectId || null, input.workspaceId || null, input.label || null, now, now);
2927
- const lastId = this.db.prepare(`SELECT last_insert_rowid() as id`).get();
2979
+ `INSERT INTO lt_a2a_api_keys (id, key_value, tenant_id, project_id, assistant_ids, label, created_at, updated_at)
2980
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
2981
+ ).run(id, encrypt3(key), input.tenantId, input.projectId, input.assistantIds ? JSON.stringify(input.assistantIds) : null, input.label || null, now, now);
2928
2982
  const record = {
2929
- id: String(lastId.id),
2983
+ id,
2930
2984
  key,
2931
2985
  tenantId: input.tenantId,
2932
2986
  projectId: input.projectId,
2933
- workspaceId: input.workspaceId,
2987
+ assistantIds: input.assistantIds,
2934
2988
  label: input.label,
2935
2989
  enabled: true,
2936
2990
  createdAt: parseISO(now),
@@ -2982,8 +3036,8 @@ var LocalA2AApiKeyStore = class {
2982
3036
  map.set(key, {
2983
3037
  key,
2984
3038
  tenantId: row.tenant_id,
2985
- projectId: row.project_id || void 0,
2986
- workspaceId: row.workspace_id || void 0
3039
+ projectId: row.project_id,
3040
+ assistantIds: parseAssistantIds(row.assistant_ids)
2987
3041
  });
2988
3042
  } catch {
2989
3043
  }
@@ -3002,8 +3056,8 @@ function mapRowToRecord(row) {
3002
3056
  id: row.id,
3003
3057
  key,
3004
3058
  tenantId: row.tenant_id,
3005
- projectId: row.project_id || void 0,
3006
- workspaceId: row.workspace_id || void 0,
3059
+ projectId: row.project_id,
3060
+ assistantIds: parseAssistantIds(row.assistant_ids),
3007
3061
  label: row.label || void 0,
3008
3062
  enabled: row.enabled === 1,
3009
3063
  createdAt: parseISO(row.created_at),
@@ -3652,6 +3706,12 @@ function mapRowToTask(row) {
3652
3706
 
3653
3707
  // src/stores/LocalTaskStore.ts
3654
3708
  import { randomUUID as randomUUID5 } from "crypto";
3709
+ var MAX_CANONICAL_TASK_TIME = "9999-12-31T23:59:59.999Z";
3710
+ var NEXT_UPDATED_AT_SQL = `updated_at = CASE
3711
+ WHEN strftime('%Y-%m-%dT%H:%M:%fZ', updated_at) = updated_at AND updated_at >= ?
3712
+ THEN strftime('%Y-%m-%dT%H:%M:%fZ', updated_at, '+0.001 seconds')
3713
+ ELSE ?
3714
+ END`;
3655
3715
  var DDL20 = `
3656
3716
  CREATE TABLE IF NOT EXISTS lt_tasks (
3657
3717
  id TEXT NOT NULL,
@@ -3715,6 +3775,10 @@ function mapRowToTask2(row) {
3715
3775
  updatedAt: parseISO(row.updated_at)
3716
3776
  };
3717
3777
  }
3778
+ function nextISO(previous) {
3779
+ const previousMs = new Date(previous).getTime();
3780
+ return new Date(Math.max(Date.now(), previousMs + 1)).toISOString();
3781
+ }
3718
3782
  var LocalTaskStore = class {
3719
3783
  constructor(db) {
3720
3784
  this.db = db;
@@ -3735,7 +3799,7 @@ var LocalTaskStore = class {
3735
3799
  }
3736
3800
  }
3737
3801
  async create(params) {
3738
- const id = randomUUID5();
3802
+ const id = params.id ?? randomUUID5();
3739
3803
  const now = nowISO();
3740
3804
  this.db.prepare(
3741
3805
  `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)
@@ -3836,8 +3900,8 @@ var LocalTaskStore = class {
3836
3900
  const existing = await this.getById(tenantId, id);
3837
3901
  if (!existing) return null;
3838
3902
  const now = nowISO();
3839
- const setClauses = ["updated_at = ?"];
3840
- const values = [now];
3903
+ const setClauses = [NEXT_UPDATED_AT_SQL];
3904
+ const values = [now, now];
3841
3905
  if (updates.title !== void 0) {
3842
3906
  setClauses.push("title = ?");
3843
3907
  values.push(updates.title);
@@ -3872,7 +3936,7 @@ var LocalTaskStore = class {
3872
3936
  }
3873
3937
  if (updates.context !== void 0) {
3874
3938
  setClauses.push("context = ?");
3875
- values.push(JSON.stringify(updates.context));
3939
+ values.push(updates.context === null ? null : JSON.stringify(updates.context));
3876
3940
  }
3877
3941
  if (updates.ownerType !== void 0) {
3878
3942
  setClauses.push("owner_type = ?");
@@ -3911,10 +3975,369 @@ var LocalTaskStore = class {
3911
3975
  values.push(JSON.stringify(updates.files));
3912
3976
  }
3913
3977
  if (setClauses.length === 1) return existing;
3914
- values.push(tenantId, id);
3915
- this.db.prepare(
3916
- `UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ?`
3978
+ values.push(tenantId, id, MAX_CANONICAL_TASK_TIME);
3979
+ const result = this.db.prepare(
3980
+ `UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ? AND updated_at <> ?`
3981
+ ).run(...values);
3982
+ if (result.changes === 0) return null;
3983
+ return this.getById(tenantId, id);
3984
+ }
3985
+ /**
3986
+ * Atomically update a task unless its current status is blocked.
3987
+ *
3988
+ * @param tenantId Tenant identifier.
3989
+ * @param id Task identifier.
3990
+ * @param updates Partial task data to update.
3991
+ * @param blockedStatuses Current statuses that prevent the update.
3992
+ * @returns The updated task, or `null` when missing or blocked.
3993
+ */
3994
+ async updateIfStatusNotIn(tenantId, id, updates, blockedStatuses) {
3995
+ const now = nowISO();
3996
+ const setClauses = [NEXT_UPDATED_AT_SQL];
3997
+ const values = [now, now];
3998
+ if (updates.title !== void 0) {
3999
+ setClauses.push("title = ?");
4000
+ values.push(updates.title);
4001
+ }
4002
+ if (updates.description !== void 0) {
4003
+ setClauses.push("description = ?");
4004
+ values.push(updates.description);
4005
+ }
4006
+ if (updates.status !== void 0) {
4007
+ setClauses.push("status = ?");
4008
+ values.push(updates.status);
4009
+ }
4010
+ if (updates.priority !== void 0) {
4011
+ setClauses.push("priority = ?");
4012
+ values.push(updates.priority);
4013
+ }
4014
+ if (updates.dueDate !== void 0) {
4015
+ setClauses.push("due_date = ?");
4016
+ values.push(updates.dueDate);
4017
+ }
4018
+ if (updates.metadata !== void 0) {
4019
+ setClauses.push("metadata = ?");
4020
+ values.push(JSON.stringify(updates.metadata));
4021
+ }
4022
+ if (updates.parentId !== void 0) {
4023
+ setClauses.push("parent_id = ?");
4024
+ values.push(updates.parentId);
4025
+ }
4026
+ if (updates.sourceId !== void 0) {
4027
+ setClauses.push("source_id = ?");
4028
+ values.push(updates.sourceId);
4029
+ }
4030
+ if (updates.context !== void 0) {
4031
+ setClauses.push("context = ?");
4032
+ values.push(updates.context === null ? null : JSON.stringify(updates.context));
4033
+ }
4034
+ if (updates.ownerType !== void 0) {
4035
+ setClauses.push("owner_type = ?");
4036
+ values.push(updates.ownerType);
4037
+ }
4038
+ if (updates.ownerId !== void 0) {
4039
+ setClauses.push("owner_id = ?");
4040
+ values.push(updates.ownerId);
4041
+ }
4042
+ if (updates.requireReview !== void 0) {
4043
+ setClauses.push("require_review = ?");
4044
+ values.push(updates.requireReview ? 1 : 0);
4045
+ }
4046
+ if (updates.dependencies !== void 0) {
4047
+ setClauses.push("dependencies = ?");
4048
+ values.push(JSON.stringify(updates.dependencies));
4049
+ }
4050
+ if (updates.result !== void 0) {
4051
+ setClauses.push("result = ?");
4052
+ values.push(updates.result);
4053
+ }
4054
+ if (updates.failureReason !== void 0) {
4055
+ setClauses.push("failure_reason = ?");
4056
+ values.push(updates.failureReason);
4057
+ }
4058
+ if (updates.workspaceId !== void 0) {
4059
+ setClauses.push("workspace_id = ?");
4060
+ values.push(updates.workspaceId);
4061
+ }
4062
+ if (updates.projectId !== void 0) {
4063
+ setClauses.push("project_id = ?");
4064
+ values.push(updates.projectId);
4065
+ }
4066
+ if (updates.files !== void 0) {
4067
+ setClauses.push("files = ?");
4068
+ values.push(JSON.stringify(updates.files));
4069
+ }
4070
+ const statusClause = blockedStatuses.length > 0 ? ` AND status NOT IN (${blockedStatuses.map(() => "?").join(", ")})` : "";
4071
+ values.push(tenantId, id, MAX_CANONICAL_TASK_TIME, ...blockedStatuses);
4072
+ const result = this.db.prepare(
4073
+ `UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ? AND updated_at <> ?${statusClause}`
4074
+ ).run(...values);
4075
+ if (result.changes === 0) return null;
4076
+ return this.getById(tenantId, id);
4077
+ }
4078
+ /** Atomically update a task only when its current status is expected. */
4079
+ async updateIfStatusIn(tenantId, id, updates, expectedStatuses) {
4080
+ if (expectedStatuses.length === 0) return null;
4081
+ const now = nowISO();
4082
+ const setClauses = [NEXT_UPDATED_AT_SQL];
4083
+ const values = [now, now];
4084
+ if (updates.title !== void 0) {
4085
+ setClauses.push("title = ?");
4086
+ values.push(updates.title);
4087
+ }
4088
+ if (updates.description !== void 0) {
4089
+ setClauses.push("description = ?");
4090
+ values.push(updates.description);
4091
+ }
4092
+ if (updates.status !== void 0) {
4093
+ setClauses.push("status = ?");
4094
+ values.push(updates.status);
4095
+ }
4096
+ if (updates.priority !== void 0) {
4097
+ setClauses.push("priority = ?");
4098
+ values.push(updates.priority);
4099
+ }
4100
+ if (updates.dueDate !== void 0) {
4101
+ setClauses.push("due_date = ?");
4102
+ values.push(updates.dueDate);
4103
+ }
4104
+ if (updates.metadata !== void 0) {
4105
+ setClauses.push("metadata = ?");
4106
+ values.push(JSON.stringify(updates.metadata));
4107
+ }
4108
+ if (updates.parentId !== void 0) {
4109
+ setClauses.push("parent_id = ?");
4110
+ values.push(updates.parentId);
4111
+ }
4112
+ if (updates.sourceId !== void 0) {
4113
+ setClauses.push("source_id = ?");
4114
+ values.push(updates.sourceId);
4115
+ }
4116
+ if (updates.context !== void 0) {
4117
+ setClauses.push("context = ?");
4118
+ values.push(updates.context === null ? null : JSON.stringify(updates.context));
4119
+ }
4120
+ if (updates.ownerType !== void 0) {
4121
+ setClauses.push("owner_type = ?");
4122
+ values.push(updates.ownerType);
4123
+ }
4124
+ if (updates.ownerId !== void 0) {
4125
+ setClauses.push("owner_id = ?");
4126
+ values.push(updates.ownerId);
4127
+ }
4128
+ if (updates.requireReview !== void 0) {
4129
+ setClauses.push("require_review = ?");
4130
+ values.push(updates.requireReview ? 1 : 0);
4131
+ }
4132
+ if (updates.dependencies !== void 0) {
4133
+ setClauses.push("dependencies = ?");
4134
+ values.push(JSON.stringify(updates.dependencies));
4135
+ }
4136
+ if (updates.result !== void 0) {
4137
+ setClauses.push("result = ?");
4138
+ values.push(updates.result);
4139
+ }
4140
+ if (updates.failureReason !== void 0) {
4141
+ setClauses.push("failure_reason = ?");
4142
+ values.push(updates.failureReason);
4143
+ }
4144
+ if (updates.workspaceId !== void 0) {
4145
+ setClauses.push("workspace_id = ?");
4146
+ values.push(updates.workspaceId);
4147
+ }
4148
+ if (updates.projectId !== void 0) {
4149
+ setClauses.push("project_id = ?");
4150
+ values.push(updates.projectId);
4151
+ }
4152
+ if (updates.files !== void 0) {
4153
+ setClauses.push("files = ?");
4154
+ values.push(JSON.stringify(updates.files));
4155
+ }
4156
+ values.push(tenantId, id, MAX_CANONICAL_TASK_TIME, ...expectedStatuses);
4157
+ const result = this.db.prepare(
4158
+ `UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ? AND updated_at <> ? AND status IN (${expectedStatuses.map(() => "?").join(", ")})`
4159
+ ).run(...values);
4160
+ if (result.changes === 0) return null;
4161
+ return this.getById(tenantId, id);
4162
+ }
4163
+ /** Atomically update a task only when status and updatedAt match a read snapshot. */
4164
+ async updateIfStatusAndUpdatedAt(tenantId, id, updates, expectedStatuses, expectedUpdatedAt) {
4165
+ if (expectedStatuses.length === 0) return null;
4166
+ const expectedIso = new Date(expectedUpdatedAt).toISOString();
4167
+ if (expectedIso === MAX_CANONICAL_TASK_TIME) return null;
4168
+ const setClauses = ["updated_at = ?"];
4169
+ const values = [nextISO(expectedIso)];
4170
+ if (updates.title !== void 0) {
4171
+ setClauses.push("title = ?");
4172
+ values.push(updates.title);
4173
+ }
4174
+ if (updates.description !== void 0) {
4175
+ setClauses.push("description = ?");
4176
+ values.push(updates.description);
4177
+ }
4178
+ if (updates.status !== void 0) {
4179
+ setClauses.push("status = ?");
4180
+ values.push(updates.status);
4181
+ }
4182
+ if (updates.priority !== void 0) {
4183
+ setClauses.push("priority = ?");
4184
+ values.push(updates.priority);
4185
+ }
4186
+ if (updates.dueDate !== void 0) {
4187
+ setClauses.push("due_date = ?");
4188
+ values.push(updates.dueDate);
4189
+ }
4190
+ if (updates.metadata !== void 0) {
4191
+ setClauses.push("metadata = ?");
4192
+ values.push(JSON.stringify(updates.metadata));
4193
+ }
4194
+ if (updates.parentId !== void 0) {
4195
+ setClauses.push("parent_id = ?");
4196
+ values.push(updates.parentId);
4197
+ }
4198
+ if (updates.sourceId !== void 0) {
4199
+ setClauses.push("source_id = ?");
4200
+ values.push(updates.sourceId);
4201
+ }
4202
+ if (updates.context !== void 0) {
4203
+ setClauses.push("context = ?");
4204
+ values.push(updates.context === null ? null : JSON.stringify(updates.context));
4205
+ }
4206
+ if (updates.ownerType !== void 0) {
4207
+ setClauses.push("owner_type = ?");
4208
+ values.push(updates.ownerType);
4209
+ }
4210
+ if (updates.ownerId !== void 0) {
4211
+ setClauses.push("owner_id = ?");
4212
+ values.push(updates.ownerId);
4213
+ }
4214
+ if (updates.requireReview !== void 0) {
4215
+ setClauses.push("require_review = ?");
4216
+ values.push(updates.requireReview ? 1 : 0);
4217
+ }
4218
+ if (updates.dependencies !== void 0) {
4219
+ setClauses.push("dependencies = ?");
4220
+ values.push(JSON.stringify(updates.dependencies));
4221
+ }
4222
+ if (updates.result !== void 0) {
4223
+ setClauses.push("result = ?");
4224
+ values.push(updates.result);
4225
+ }
4226
+ if (updates.failureReason !== void 0) {
4227
+ setClauses.push("failure_reason = ?");
4228
+ values.push(updates.failureReason);
4229
+ }
4230
+ if (updates.workspaceId !== void 0) {
4231
+ setClauses.push("workspace_id = ?");
4232
+ values.push(updates.workspaceId);
4233
+ }
4234
+ if (updates.projectId !== void 0) {
4235
+ setClauses.push("project_id = ?");
4236
+ values.push(updates.projectId);
4237
+ }
4238
+ if (updates.files !== void 0) {
4239
+ setClauses.push("files = ?");
4240
+ values.push(JSON.stringify(updates.files));
4241
+ }
4242
+ values.push(tenantId, id, ...expectedStatuses, expectedIso);
4243
+ const result = this.db.prepare(
4244
+ `UPDATE lt_tasks SET ${setClauses.join(", ")} WHERE tenant_id = ? AND id = ? AND status IN (${expectedStatuses.map(() => "?").join(", ")}) AND updated_at = ?`
4245
+ ).run(...values);
4246
+ if (result.changes === 0) return null;
4247
+ return this.getById(tenantId, id);
4248
+ }
4249
+ /** Atomically update a child only when both child and parent snapshots match. */
4250
+ async updateIfStatusUpdatedAtAndParentUpdatedAt(tenantId, id, updates, expectedStatuses, expectedUpdatedAt, parentId, expectedParentUpdatedAt) {
4251
+ if (expectedStatuses.length === 0) return null;
4252
+ const expectedIso = new Date(expectedUpdatedAt).toISOString();
4253
+ if (expectedIso === MAX_CANONICAL_TASK_TIME) return null;
4254
+ const parentExpectedIso = new Date(expectedParentUpdatedAt).toISOString();
4255
+ const setClauses = ["updated_at = ?"];
4256
+ const values = [nextISO(expectedIso)];
4257
+ if (updates.title !== void 0) {
4258
+ setClauses.push("title = ?");
4259
+ values.push(updates.title);
4260
+ }
4261
+ if (updates.description !== void 0) {
4262
+ setClauses.push("description = ?");
4263
+ values.push(updates.description);
4264
+ }
4265
+ if (updates.status !== void 0) {
4266
+ setClauses.push("status = ?");
4267
+ values.push(updates.status);
4268
+ }
4269
+ if (updates.priority !== void 0) {
4270
+ setClauses.push("priority = ?");
4271
+ values.push(updates.priority);
4272
+ }
4273
+ if (updates.dueDate !== void 0) {
4274
+ setClauses.push("due_date = ?");
4275
+ values.push(updates.dueDate);
4276
+ }
4277
+ if (updates.metadata !== void 0) {
4278
+ setClauses.push("metadata = ?");
4279
+ values.push(JSON.stringify(updates.metadata));
4280
+ }
4281
+ if (updates.parentId !== void 0) {
4282
+ setClauses.push("parent_id = ?");
4283
+ values.push(updates.parentId);
4284
+ }
4285
+ if (updates.sourceId !== void 0) {
4286
+ setClauses.push("source_id = ?");
4287
+ values.push(updates.sourceId);
4288
+ }
4289
+ if (updates.context !== void 0) {
4290
+ setClauses.push("context = ?");
4291
+ values.push(updates.context === null ? null : JSON.stringify(updates.context));
4292
+ }
4293
+ if (updates.ownerType !== void 0) {
4294
+ setClauses.push("owner_type = ?");
4295
+ values.push(updates.ownerType);
4296
+ }
4297
+ if (updates.ownerId !== void 0) {
4298
+ setClauses.push("owner_id = ?");
4299
+ values.push(updates.ownerId);
4300
+ }
4301
+ if (updates.requireReview !== void 0) {
4302
+ setClauses.push("require_review = ?");
4303
+ values.push(updates.requireReview ? 1 : 0);
4304
+ }
4305
+ if (updates.dependencies !== void 0) {
4306
+ setClauses.push("dependencies = ?");
4307
+ values.push(JSON.stringify(updates.dependencies));
4308
+ }
4309
+ if (updates.result !== void 0) {
4310
+ setClauses.push("result = ?");
4311
+ values.push(updates.result);
4312
+ }
4313
+ if (updates.failureReason !== void 0) {
4314
+ setClauses.push("failure_reason = ?");
4315
+ values.push(updates.failureReason);
4316
+ }
4317
+ if (updates.workspaceId !== void 0) {
4318
+ setClauses.push("workspace_id = ?");
4319
+ values.push(updates.workspaceId);
4320
+ }
4321
+ if (updates.projectId !== void 0) {
4322
+ setClauses.push("project_id = ?");
4323
+ values.push(updates.projectId);
4324
+ }
4325
+ if (updates.files !== void 0) {
4326
+ setClauses.push("files = ?");
4327
+ values.push(JSON.stringify(updates.files));
4328
+ }
4329
+ values.push(tenantId, id, ...expectedStatuses, expectedIso, parentId, parentExpectedIso);
4330
+ const result = this.db.prepare(
4331
+ `UPDATE lt_tasks AS child SET ${setClauses.join(", ")}
4332
+ WHERE tenant_id = ? AND id = ?
4333
+ AND status IN (${expectedStatuses.map(() => "?").join(", ")})
4334
+ AND updated_at = ?
4335
+ AND EXISTS (
4336
+ SELECT 1 FROM lt_tasks AS parent
4337
+ WHERE parent.tenant_id = child.tenant_id AND parent.id = ? AND parent.updated_at = ?
4338
+ )`
3917
4339
  ).run(...values);
4340
+ if (result.changes === 0) return null;
3918
4341
  return this.getById(tenantId, id);
3919
4342
  }
3920
4343
  async delete(tenantId, id) {
@@ -3957,6 +4380,7 @@ function mapRowToWorkItem(row) {
3957
4380
  attempt: row.attempt ?? void 0,
3958
4381
  workspaceId: row.workspace_id ?? void 0,
3959
4382
  projectId: row.project_id ?? void 0,
4383
+ eventKey: row.event_key ?? void 0,
3960
4384
  createdAt: parseISO(row.created_at)
3961
4385
  };
3962
4386
  }
@@ -3964,8 +4388,18 @@ var LocalTaskWorkItemStore = class {
3964
4388
  constructor(db) {
3965
4389
  this.db = db;
3966
4390
  ensureTable(db, DDL21);
3967
- this.db.exec(`ALTER TABLE lt_task_work_items ADD COLUMN workspace_id TEXT;`);
3968
- this.db.exec(`ALTER TABLE lt_task_work_items ADD COLUMN project_id TEXT;`);
4391
+ this.ensureColumn("lt_task_work_items", "workspace_id", "workspace_id TEXT");
4392
+ this.ensureColumn("lt_task_work_items", "project_id", "project_id TEXT");
4393
+ this.ensureColumn("lt_task_work_items", "event_key", "event_key TEXT");
4394
+ this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_task_work_items_event_key
4395
+ ON lt_task_work_items (tenant_id, task_id, event_key);`);
4396
+ }
4397
+ /** Add a column if it does not exist (SQLite version compatible). */
4398
+ ensureColumn(table, column, ddl) {
4399
+ const cols = this.db.prepare(`PRAGMA table_info(${table})`).all();
4400
+ if (!cols.some((c) => c.name === column)) {
4401
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
4402
+ }
3969
4403
  }
3970
4404
  async create(params) {
3971
4405
  const id = uuid2();
@@ -3992,6 +4426,39 @@ var LocalTaskWorkItemStore = class {
3992
4426
  ).get(params.tenantId, id);
3993
4427
  return mapRowToWorkItem(row);
3994
4428
  }
4429
+ /** Find an event by its tenant- and task-scoped key without pagination. */
4430
+ async findByEventKey(tenantId, taskId, eventKey) {
4431
+ const row = this.db.prepare(
4432
+ `SELECT * FROM lt_task_work_items WHERE tenant_id = ? AND task_id = ? AND event_key = ?`
4433
+ ).get(tenantId, taskId, eventKey);
4434
+ return row ? mapRowToWorkItem(row) : null;
4435
+ }
4436
+ /** Atomically return an existing event or create it once. */
4437
+ async createIfAbsentByEventKey(params) {
4438
+ const id = uuid2();
4439
+ this.db.prepare(
4440
+ `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)
4441
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4442
+ ON CONFLICT (tenant_id, task_id, event_key) DO NOTHING`
4443
+ ).run(
4444
+ id,
4445
+ params.tenantId,
4446
+ params.taskId,
4447
+ params.action,
4448
+ params.actor,
4449
+ params.threadId || null,
4450
+ params.summary || null,
4451
+ params.detail ? JSON.stringify(params.detail) : null,
4452
+ params.attempt ?? null,
4453
+ params.workspaceId || null,
4454
+ params.projectId || null,
4455
+ params.eventKey,
4456
+ nowISO()
4457
+ );
4458
+ const item = await this.findByEventKey(params.tenantId, params.taskId, params.eventKey);
4459
+ if (!item) throw new Error("Task work item insert did not return a row");
4460
+ return item;
4461
+ }
3995
4462
  async list(filter) {
3996
4463
  const conditions = ["tenant_id = ?", "task_id = ?"];
3997
4464
  const params = [filter.tenantId, filter.taskId];
@@ -3999,11 +4466,20 @@ var LocalTaskWorkItemStore = class {
3999
4466
  conditions.push("action = ?");
4000
4467
  params.push(filter.action);
4001
4468
  }
4469
+ if (filter.workspaceId) {
4470
+ conditions.push("workspace_id = ?");
4471
+ params.push(filter.workspaceId);
4472
+ }
4473
+ if (filter.projectId) {
4474
+ conditions.push("project_id = ?");
4475
+ params.push(filter.projectId);
4476
+ }
4002
4477
  const where = conditions.join(" AND ");
4003
4478
  const limit = filter.limit || 100;
4004
4479
  const offset = filter.offset || 0;
4480
+ const order = filter.order === "asc" ? "ASC" : "DESC";
4005
4481
  const rows = this.db.prepare(
4006
- `SELECT * FROM lt_task_work_items WHERE ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`
4482
+ `SELECT * FROM lt_task_work_items WHERE ${where} ORDER BY created_at ${order}, id ${order} LIMIT ? OFFSET ?`
4007
4483
  ).all(...params, limit, offset);
4008
4484
  return rows.map(mapRowToWorkItem);
4009
4485
  }