@axiom-lattice/local-stores 3.1.23 → 3.2.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.mjs CHANGED
@@ -4506,6 +4506,9 @@ CREATE TABLE IF NOT EXISTS lt_thread_message_queue (
4506
4506
  custom_run_config TEXT,
4507
4507
  trusted_run_context TEXT,
4508
4508
  execution_mode TEXT CHECK (execution_mode IS NULL OR execution_mode = 'followup'),
4509
+ locked_by TEXT,
4510
+ locked_at TEXT,
4511
+ lease_expires_at TEXT,
4509
4512
  created_at TEXT NOT NULL
4510
4513
  );
4511
4514
  CREATE INDEX IF NOT EXISTS idx_lt_tmq_thread ON lt_thread_message_queue(thread_id, status);
@@ -4524,6 +4527,16 @@ var LocalThreadMessageQueueStore = class {
4524
4527
  if (!names.has("execution_mode")) {
4525
4528
  db.run("ALTER TABLE lt_thread_message_queue ADD COLUMN execution_mode TEXT CHECK (execution_mode IS NULL OR execution_mode = 'followup')");
4526
4529
  }
4530
+ if (!names.has("locked_by")) {
4531
+ db.run("ALTER TABLE lt_thread_message_queue ADD COLUMN locked_by TEXT");
4532
+ }
4533
+ if (!names.has("locked_at")) {
4534
+ db.run("ALTER TABLE lt_thread_message_queue ADD COLUMN locked_at TEXT");
4535
+ }
4536
+ if (!names.has("lease_expires_at")) {
4537
+ db.run("ALTER TABLE lt_thread_message_queue ADD COLUMN lease_expires_at TEXT");
4538
+ }
4539
+ db.run("CREATE INDEX IF NOT EXISTS idx_lt_tmq_lease ON lt_thread_message_queue(status, lease_expires_at)");
4527
4540
  }
4528
4541
  async addMessage(params) {
4529
4542
  const trusted = validateQueueTrust(params);
@@ -4648,6 +4661,37 @@ var LocalThreadMessageQueueStore = class {
4648
4661
  ).all(threadId, ...filter.params);
4649
4662
  return rows2.map(rowToMessage);
4650
4663
  }
4664
+ async claimPendingMessages(threadId, scope, options) {
4665
+ const filter = scopeClause(scope);
4666
+ const limitClause = options.limit === void 0 ? "" : ` LIMIT ${Math.max(0, Math.floor(options.limit))}`;
4667
+ const claimedRows = transaction(this.db, () => {
4668
+ const candidates = this.db.prepare(
4669
+ `SELECT * FROM lt_thread_message_queue WHERE thread_id = ? AND status = 'pending'${filter.sql} ORDER BY priority DESC, sequence_order ASC${limitClause}`
4670
+ ).all(threadId, ...filter.params);
4671
+ const startedAt = Date.now();
4672
+ const lockedAt = new Date(startedAt).toISOString();
4673
+ const leaseExpiresAt = new Date(startedAt + options.ttlMs).toISOString();
4674
+ const claimed = [];
4675
+ for (const row of candidates) {
4676
+ const changes = write(
4677
+ this.db,
4678
+ `UPDATE lt_thread_message_queue SET status = 'processing', locked_by = ?, locked_at = ?, lease_expires_at = ? WHERE id = ? AND status = 'pending'`,
4679
+ [options.owner, lockedAt, leaseExpiresAt, row.id]
4680
+ );
4681
+ if (changes > 0) claimed.push(row);
4682
+ }
4683
+ return claimed;
4684
+ });
4685
+ return claimedRows.map(rowToMessage);
4686
+ }
4687
+ async reclaimExpiredProcessing(threadId, scope) {
4688
+ const filter = scopeClause(scope);
4689
+ return transaction(this.db, () => write(
4690
+ this.db,
4691
+ `UPDATE lt_thread_message_queue SET status = 'pending', locked_by = NULL, locked_at = NULL, lease_expires_at = NULL WHERE thread_id = ? AND status = 'processing' AND lease_expires_at IS NOT NULL AND lease_expires_at <= ?${filter.sql}`,
4692
+ [threadId, nowISO(), ...filter.params]
4693
+ ));
4694
+ }
4651
4695
  async getQueueSize(threadId, scope) {
4652
4696
  const filter = scopeClause(scope);
4653
4697
  const row = this.db.prepare(
@@ -4689,7 +4733,7 @@ var LocalThreadMessageQueueStore = class {
4689
4733
  async resetProcessingToPending(threadId, scope) {
4690
4734
  const filter = scopeClause(scope);
4691
4735
  const result = this.db.prepare(
4692
- `UPDATE lt_thread_message_queue SET status = 'pending' WHERE thread_id = ? AND status = 'processing'${filter.sql}`
4736
+ `UPDATE lt_thread_message_queue SET status = 'pending', locked_by = NULL, locked_at = NULL, lease_expires_at = NULL WHERE thread_id = ? AND status = 'processing'${filter.sql}`
4693
4737
  ).run(threadId, ...filter.params);
4694
4738
  return result.changes;
4695
4739
  }
@@ -5075,6 +5119,27 @@ var LocalScheduleStorage = class {
5075
5119
  `UPDATE lt_scheduled_tasks SET ${setClauses.join(", ")} WHERE task_id = ?`
5076
5120
  ).run(...values);
5077
5121
  }
5122
+ async claimScheduledRun(taskId) {
5123
+ const result = this.db.prepare(
5124
+ `UPDATE lt_scheduled_tasks SET status = 'running', updated_at = ? WHERE task_id = ? AND status = 'pending'`
5125
+ ).run((/* @__PURE__ */ new Date()).toISOString(), taskId);
5126
+ return result.changes > 0;
5127
+ }
5128
+ /**
5129
+ * Requeues `running` tasks whose `updated_at` is older than `olderThanMs` back
5130
+ * to `pending` (recovery after a crash that left a claim unsettled).
5131
+ *
5132
+ * @param olderThanMs Age threshold in milliseconds.
5133
+ * @returns The number of tasks requeued.
5134
+ */
5135
+ async requeueStaleRunning(olderThanMs) {
5136
+ const now = /* @__PURE__ */ new Date();
5137
+ const cutoff = new Date(now.getTime() - olderThanMs).toISOString();
5138
+ const result = this.db.prepare(
5139
+ `UPDATE lt_scheduled_tasks SET status = 'pending', updated_at = ? WHERE status = 'running' AND updated_at < ?`
5140
+ ).run(now.toISOString(), cutoff);
5141
+ return result.changes;
5142
+ }
5078
5143
  async delete(taskId) {
5079
5144
  this.db.prepare(`DELETE FROM lt_scheduled_tasks WHERE task_id = ?`).run(taskId);
5080
5145
  }