@rei-standard/amsg-server 2.6.0-next.34 → 2.6.0-next.35

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.
@@ -67,7 +67,7 @@ export class D1Adapter {
67
67
  data: any;
68
68
  updatedAt: any;
69
69
  }>;
70
- putCloudDataRecord(userId: any, kind: any, id: any, data: any, { idempotencyKey, createOnly, leaseToken }?: {
70
+ putCloudDataRecord(userId: any, kind: any, id: any, data: any, { idempotencyKey, createOnly, leaseToken, work }?: {
71
71
  idempotencyKey?: any;
72
72
  createOnly?: boolean;
73
73
  }): Promise<{
@@ -105,8 +105,31 @@ export class D1Adapter {
105
105
  renewCloudDataRecordLease(userId: any, kind: any, id: any, leaseMs: any, leaseToken: any): Promise<boolean>;
106
106
  deleteCloudDataRecord(userId: any, kind: any, id: any): Promise<boolean>;
107
107
  cleanupCloudDataRecords(kind: any, beforeMs: any): Promise<any>;
108
- /** Sidecars are indexes only. Remove them only while their backing row is absent. */
109
- cleanupCloudResourceMetadata(userId?: any): Promise<any>;
108
+ listDueCloudDataOperations(now?: number, limit?: number): Promise<any[]>;
109
+ cleanupExpiredCloudDataRecords(now?: number, limit?: number): Promise<any>;
110
+ getCloudMaintenanceBatch(name: any, table: any, limit?: number): Promise<{
111
+ rows: any[];
112
+ completed: boolean;
113
+ upperBound?: undefined;
114
+ cursor?: undefined;
115
+ } | {
116
+ rows: any;
117
+ upperBound: any;
118
+ cursor: any;
119
+ completed: boolean;
120
+ }>;
121
+ finishCloudMaintenanceBatch(name: any, cursor: any, completed: any): Promise<void>;
122
+ indexCloudDataRecordWork(userId: any, kind: any, id: any, work: any, expectedWriteToken?: any): Promise<void>;
123
+ readCloudMaintenanceRecord(row: any): Promise<{
124
+ id: any;
125
+ userId: any;
126
+ kind: any;
127
+ data: any;
128
+ updatedAt: any;
129
+ }>;
130
+ repairCloudResourceMetadata(limit?: number): Promise<void>;
131
+ /** One explicit repair, or a bounded upgrade batch; never called as a recurring sweep. */
132
+ cleanupCloudResourceMetadata(userId?: any, range?: any): Promise<any>;
110
133
  listCloudOwners(userId: any): Promise<any>;
111
134
  getCloudOwner(userId: any, owner: any): Promise<{
112
135
  active: boolean;
@@ -151,11 +174,12 @@ export class D1Adapter {
151
174
  * 构变了而老部署没跑过 initSchema 时,cron 会每分钟静默挂在缺的那一列上,
152
175
  * 界面上一切正常——这个方法就是让宿主查得出来。
153
176
  *
154
- * @returns {Promise<{ tables: Record<string, string[]>, indexes: string[] }>}
177
+ * @returns {Promise<{ tables: Record<string, string[]>, indexes: string[], triggers: string[] }>}
155
178
  */
156
179
  describeSchema(): Promise<{
157
180
  tables: Record<string, string[]>;
158
181
  indexes: string[];
182
+ triggers: string[];
159
183
  }>;
160
184
  dropSchema(): Promise<void>;
161
185
  createTask(params: any, cloudGuard?: any): Promise<any>;
@@ -49,6 +49,7 @@ export type DbAdapter = {
49
49
  describeSchema?: () => Promise<{
50
50
  tables: Record<string, string[]>;
51
51
  indexes: string[];
52
+ triggers?: string[];
52
53
  }>;
53
54
  /**
54
55
  * Drop the scheduled_messages table (CASCADE).
@@ -54,15 +54,27 @@ export const SQLITE_ALL_INDEXES: {
54
54
  critical: boolean;
55
55
  }[];
56
56
  export const CLOUD_DATA_TABLES_SQL: string[];
57
+ export const CLOUD_DATA_WORK_TABLES_SQL: string[];
58
+ export const CLOUD_DATA_WORK_INDEXES: {
59
+ name: string;
60
+ sql: string;
61
+ description: string;
62
+ critical: boolean;
63
+ }[];
64
+ export const CLOUD_DATA_DELETE_TRIGGERS: {
65
+ name: string;
66
+ sql: string;
67
+ }[];
57
68
  /**
58
69
  * 这一版代码跑起来需要的表 / 列 / 索引。
59
70
  *
60
71
  * 索引只列 critical 的那几个(uidx_uuid 之类):其余索引缺了只是慢,缺了它则
61
72
  * 是正确性问题。
62
73
  *
63
- * @type {{ tables: Record<string, string[]>, indexes: string[] }}
74
+ * @type {{ tables: Record<string, string[]>, indexes: string[], triggers: string[] }}
64
75
  */
65
76
  export const SQLITE_REQUIRED_SCHEMA: {
66
77
  tables: Record<string, string[]>;
67
78
  indexes: string[];
79
+ triggers: string[];
68
80
  };
@@ -5070,6 +5070,66 @@ var CLOUD_DATA_TABLES_SQL = [
5070
5070
  lease_allowed INTEGER CONSTRAINT cloud_lease_guard CHECK (lease_allowed = 1)
5071
5071
  )`
5072
5072
  ];
5073
+ var CLOUD_DATA_WORK_TABLES_SQL = [
5074
+ `CREATE TABLE IF NOT EXISTS cloud_data_work (
5075
+ user_id TEXT NOT NULL, kind TEXT NOT NULL, id TEXT NOT NULL,
5076
+ next_run_at INTEGER, expires_at INTEGER,
5077
+ PRIMARY KEY (user_id, kind, id)
5078
+ )`,
5079
+ `CREATE TABLE IF NOT EXISTS cloud_data_maintenance (
5080
+ name TEXT PRIMARY KEY, cursor INTEGER NOT NULL, upper_bound INTEGER NOT NULL,
5081
+ completed INTEGER NOT NULL DEFAULT 0
5082
+ )`
5083
+ ];
5084
+ CLOUD_DATA_TABLES_SQL.push(...CLOUD_DATA_WORK_TABLES_SQL);
5085
+ var CLOUD_DATA_WORK_INDEXES = [
5086
+ {
5087
+ name: "idx_cloud_work_due",
5088
+ sql: `CREATE INDEX IF NOT EXISTS idx_cloud_work_due
5089
+ ON cloud_data_work (kind, next_run_at) WHERE next_run_at IS NOT NULL`,
5090
+ description: "Only due cleanup operations",
5091
+ critical: true
5092
+ },
5093
+ {
5094
+ name: "idx_cloud_work_expiry",
5095
+ sql: `CREATE INDEX IF NOT EXISTS idx_cloud_work_expiry
5096
+ ON cloud_data_work (expires_at) WHERE expires_at IS NOT NULL`,
5097
+ description: "Only expired management records",
5098
+ critical: true
5099
+ }
5100
+ ];
5101
+ SQLITE_ALL_INDEXES.push(...CLOUD_DATA_WORK_INDEXES);
5102
+ var chunkPrefixSql = "char(31)||'amsg-chunks'||char(31)";
5103
+ var logicalNamespaceSql = `CASE WHEN substr(OLD.namespace,1,13)=${chunkPrefixSql}
5104
+ THEN substr(OLD.namespace,14) ELSE OLD.namespace END`;
5105
+ var logicalKeySql = `CASE WHEN substr(OLD.namespace,1,13)=${chunkPrefixSql} AND instr(OLD.key,char(31))>0
5106
+ THEN substr(OLD.key,1,instr(OLD.key,char(31))-1) ELSE OLD.key END`;
5107
+ var CLOUD_DATA_DELETE_TRIGGERS = [
5108
+ ...[
5109
+ ["scheduled_messages", "task", "uuid"],
5110
+ ["llm_credentials", "credential", "cred_id"],
5111
+ ["message_outbox", "outbox", "message_id"],
5112
+ ["push_subscriptions", "subscription", "user_id"]
5113
+ ].map(([table, type, column]) => ({
5114
+ name: `trg_cloud_metadata_${type}_delete`,
5115
+ sql: `CREATE TRIGGER IF NOT EXISTS trg_cloud_metadata_${type}_delete AFTER DELETE ON ${table}
5116
+ BEGIN DELETE FROM cloud_resource_metadata WHERE user_id=OLD.user_id
5117
+ AND resource_key=json_array('${type}'${column ? ",OLD." + column : ""}); END`
5118
+ })),
5119
+ { name: "trg_cloud_metadata_state_delete", sql: `CREATE TRIGGER IF NOT EXISTS trg_cloud_metadata_state_delete
5120
+ AFTER DELETE ON client_state BEGIN
5121
+ DELETE FROM cloud_resource_metadata WHERE user_id=OLD.user_id
5122
+ AND resource_key=json_array('state',${logicalNamespaceSql},${logicalKeySql})
5123
+ AND NOT EXISTS (SELECT 1 FROM client_state r WHERE r.user_id=OLD.user_id
5124
+ AND r.namespace=(${logicalNamespaceSql}) AND r.key=(${logicalKeySql}))
5125
+ AND NOT EXISTS (SELECT 1 FROM client_state r WHERE r.user_id=OLD.user_id
5126
+ AND r.namespace=${chunkPrefixSql}||(${logicalNamespaceSql})
5127
+ AND r.key >= (${logicalKeySql})||char(31) AND r.key < (${logicalKeySql})||char(32)); END` },
5128
+ { name: "trg_cloud_work_delete", sql: `CREATE TRIGGER IF NOT EXISTS trg_cloud_work_delete
5129
+ AFTER DELETE ON cloud_data_records BEGIN
5130
+ DELETE FROM cloud_data_work WHERE user_id=OLD.user_id AND kind=OLD.kind AND id=OLD.id;
5131
+ DELETE FROM cloud_data_record_chunks WHERE user_id=OLD.user_id AND kind=OLD.kind AND id=OLD.id; END` }
5132
+ ];
5073
5133
  function parseTableName(sql) {
5074
5134
  const match = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([A-Za-z_][A-Za-z0-9_]*)/i.exec(sql);
5075
5135
  return match ? match[1] : "";
@@ -5105,7 +5165,8 @@ var SQLITE_REQUIRED_SCHEMA = Object.freeze({
5105
5165
  describeTable(MESSAGE_OUTBOX_TABLE_SQL),
5106
5166
  ...CLOUD_DATA_TABLES_SQL.map(describeTable)
5107
5167
  ])),
5108
- indexes: Object.freeze(SQLITE_ALL_INDEXES.filter((index) => index.critical).map((index) => index.name))
5168
+ indexes: Object.freeze(SQLITE_ALL_INDEXES.filter((index) => index.critical).map((index) => index.name)),
5169
+ triggers: Object.freeze(CLOUD_DATA_DELETE_TRIGGERS.map((trigger) => trigger.name))
5109
5170
  });
5110
5171
 
5111
5172
  // src/server/adapters/d1.js
@@ -5304,7 +5365,7 @@ var D1Adapter = class {
5304
5365
  }
5305
5366
  return { id: row.id, userId: row.user_id, kind: row.kind, data, updatedAt: row.updated_at };
5306
5367
  }
5307
- async putCloudDataRecord(userId, kind, id, data, { idempotencyKey = null, createOnly = false, leaseToken } = {}) {
5368
+ async putCloudDataRecord(userId, kind, id, data, { idempotencyKey = null, createOnly = false, leaseToken, work } = {}) {
5308
5369
  const slices = [];
5309
5370
  if (data.length > 200 * 1024) for (let offset = 0; offset < data.length; offset += 200 * 1024) slices.push(data.slice(offset, offset + 200 * 1024));
5310
5371
  const rootValue = slices.length ? "amsg-record" + slices.length : data;
@@ -5331,6 +5392,12 @@ var D1Adapter = class {
5331
5392
  SELECT ?,?,?,?,?,? WHERE ${rootGate}`
5332
5393
  ).bind(userId, kind, id, index, slices[index], writeToken, userId, kind, id, writeToken));
5333
5394
  }
5395
+ const schedule = work ?? (["inventory", "plan"].includes(kind) ? { nextRunAt: null, expiresAt: now + 36e5 } : null);
5396
+ if (schedule) statements.push(this._db.prepare(
5397
+ `INSERT INTO cloud_data_work (user_id,kind,id,next_run_at,expires_at)
5398
+ SELECT ?,?,?,?,? WHERE ${rootGate}
5399
+ ON CONFLICT (user_id,kind,id) DO UPDATE SET next_run_at=excluded.next_run_at,expires_at=excluded.expires_at`
5400
+ ).bind(userId, kind, id, schedule.nextRunAt ?? null, schedule.expiresAt ?? null, userId, kind, id, writeToken));
5334
5401
  if (!this.cloudDataManagement) throw new Error("Cloud management requires transactional batches");
5335
5402
  const results = await this._db.batch(statements);
5336
5403
  if (leaseToken !== void 0 && !results[0].meta.changes) throw Object.assign(new Error("Cloud operation processing lease was lost"), { code: "CLOUD_LEASE_LOST" });
@@ -5408,23 +5475,85 @@ var D1Adapter = class {
5408
5475
  ]);
5409
5476
  return results[1].meta.changes || 0;
5410
5477
  }
5411
- /** Sidecars are indexes only. Remove them only while their backing row is absent. */
5412
- async cleanupCloudResourceMetadata(userId = null) {
5478
+ async listDueCloudDataOperations(now = Date.now(), limit = 25) {
5479
+ const result = await this._db.prepare(`SELECT r.* FROM cloud_data_work w JOIN cloud_data_records r
5480
+ ON r.user_id=w.user_id AND r.kind=w.kind AND r.id=w.id
5481
+ WHERE w.kind='operation' AND w.next_run_at IS NOT NULL AND w.next_run_at<=?
5482
+ AND (r.lease_until IS NULL OR r.lease_until<=?)
5483
+ ORDER BY w.next_run_at LIMIT ?`).bind(now, now, limit).all();
5484
+ return Promise.all((result.results || []).map((row) => this._cloudRecordForList(row)));
5485
+ }
5486
+ async cleanupExpiredCloudDataRecords(now = Date.now(), limit = 100) {
5487
+ const result = await this._db.prepare(`DELETE FROM cloud_data_records WHERE rowid IN (
5488
+ SELECT r.rowid FROM cloud_data_work w JOIN cloud_data_records r
5489
+ ON r.user_id=w.user_id AND r.kind=w.kind AND r.id=w.id
5490
+ WHERE w.expires_at IS NOT NULL AND w.expires_at<=?
5491
+ AND (r.lease_until IS NULL OR r.lease_until<=?) ORDER BY w.expires_at LIMIT ?
5492
+ )`).bind(now, now, limit).run();
5493
+ return result.meta.changes || 0;
5494
+ }
5495
+ async getCloudMaintenanceBatch(name, table, limit = 100) {
5496
+ if (!["cloud_resource_metadata", "cloud_data_records"].includes(table)) throw new Error("Invalid maintenance source");
5497
+ let marker = await this._db.prepare("SELECT * FROM cloud_data_maintenance WHERE name=?").bind(name).first();
5498
+ if (!marker) {
5499
+ await this._db.prepare(`INSERT INTO cloud_data_maintenance (name,cursor,upper_bound,completed)
5500
+ SELECT ?,0,COALESCE(MAX(rowid),0),0 FROM ${table} WHERE 1 ON CONFLICT (name) DO NOTHING`).bind(name).run();
5501
+ marker = await this._db.prepare("SELECT * FROM cloud_data_maintenance WHERE name=?").bind(name).first();
5502
+ }
5503
+ if (marker.completed) return { rows: [], completed: true };
5504
+ const result = await this._db.prepare(`SELECT rowid AS maintenance_rowid,* FROM ${table}
5505
+ WHERE rowid>? AND rowid<=? ORDER BY rowid LIMIT ?`).bind(marker.cursor, marker.upper_bound, limit).all();
5506
+ return { rows: result.results || [], upperBound: marker.upper_bound, cursor: marker.cursor, completed: false };
5507
+ }
5508
+ async finishCloudMaintenanceBatch(name, cursor, completed) {
5509
+ await this._db.prepare(`UPDATE cloud_data_maintenance SET cursor=MAX(cursor,?),completed=MAX(completed,?) WHERE name=?`).bind(cursor, completed ? 1 : 0, name).run();
5510
+ }
5511
+ async indexCloudDataRecordWork(userId, kind, id, work, expectedWriteToken = null) {
5512
+ await this._db.prepare(`INSERT INTO cloud_data_work (user_id,kind,id,next_run_at,expires_at)
5513
+ SELECT user_id,kind,id,?,? FROM cloud_data_records WHERE user_id=? AND kind=? AND id=? AND write_token IS ?
5514
+ ON CONFLICT (user_id,kind,id) DO NOTHING`).bind(work.nextRunAt ?? null, work.expiresAt ?? null, userId, kind, id, expectedWriteToken).run();
5515
+ }
5516
+ async readCloudMaintenanceRecord(row) {
5517
+ return this._cloudRecordForList(row);
5518
+ }
5519
+ async repairCloudResourceMetadata(limit = 100) {
5520
+ const name = "metadata-sidecars-v1";
5521
+ const batch = await this.getCloudMaintenanceBatch(name, "cloud_resource_metadata", limit);
5522
+ if (batch.completed) return;
5523
+ const cursor = batch.rows.at(-1)?.maintenance_rowid ?? batch.upperBound;
5524
+ if (batch.rows.length) await this.cleanupCloudResourceMetadata(null, { after: batch.cursor, through: cursor });
5525
+ await this.finishCloudMaintenanceBatch(name, cursor, batch.rows.length < limit || cursor >= batch.upperBound);
5526
+ }
5527
+ /** One explicit repair, or a bounded upgrade batch; never called as a recurring sweep. */
5528
+ async cleanupCloudResourceMetadata(userId = null, range = null) {
5413
5529
  const targets = {
5414
- task: `SELECT 1 FROM scheduled_messages r WHERE r.user_id=m.user_id AND r.uuid=json_extract(m.resource_key,'$[1]')`,
5415
- state: `SELECT 1 FROM client_state r WHERE r.user_id=m.user_id AND
5416
- ((r.namespace=json_extract(m.resource_key,'$[1]') AND r.key=json_extract(m.resource_key,'$[2]')) OR
5417
- (r.namespace=char(31)||'amsg-chunks'||char(31)||json_extract(m.resource_key,'$[1]')
5530
+ task: [`SELECT 1 FROM scheduled_messages r WHERE r.user_id=m.user_id AND r.uuid=json_extract(m.resource_key,'$[1]')`],
5531
+ state: [
5532
+ `SELECT 1 FROM client_state r WHERE r.user_id=m.user_id
5533
+ AND r.namespace=json_extract(m.resource_key,'$[1]') AND r.key=json_extract(m.resource_key,'$[2]')`,
5534
+ `SELECT 1 FROM client_state r WHERE r.user_id=m.user_id
5535
+ AND r.namespace=char(31)||'amsg-chunks'||char(31)||json_extract(m.resource_key,'$[1]')
5418
5536
  AND r.key >= json_extract(m.resource_key,'$[2]')||char(31)
5419
- AND r.key < json_extract(m.resource_key,'$[2]')||char(32)))`,
5420
- credential: `SELECT 1 FROM llm_credentials r WHERE r.user_id=m.user_id AND r.cred_id=json_extract(m.resource_key,'$[1]')`,
5421
- outbox: `SELECT 1 FROM message_outbox r WHERE r.user_id=m.user_id AND r.message_id=json_extract(m.resource_key,'$[1]')`,
5422
- subscription: `SELECT 1 FROM push_subscriptions r WHERE r.user_id=m.user_id`
5537
+ AND r.key < json_extract(m.resource_key,'$[2]')||char(32)`
5538
+ ],
5539
+ credential: [`SELECT 1 FROM llm_credentials r WHERE r.user_id=m.user_id AND r.cred_id=json_extract(m.resource_key,'$[1]')`],
5540
+ outbox: [`SELECT 1 FROM message_outbox r WHERE r.user_id=m.user_id AND r.message_id=json_extract(m.resource_key,'$[1]')`],
5541
+ subscription: [`SELECT 1 FROM push_subscriptions r WHERE r.user_id=m.user_id`]
5423
5542
  };
5424
- const statements = Object.entries(targets).map(([type, exists]) => this._db.prepare(
5425
- `DELETE FROM cloud_resource_metadata AS m WHERE (? IS NULL OR user_id=?) AND json_valid(resource_key)
5426
- AND json_extract(resource_key,'$[0]')=? AND NOT EXISTS (${exists})`
5427
- ).bind(userId, userId, type));
5543
+ const statements = Object.entries(targets).map(([type, exists]) => {
5544
+ const conditions = ["json_valid(resource_key)", "json_extract(resource_key,'$[0]')=?"];
5545
+ const args = [type];
5546
+ if (userId !== null) {
5547
+ conditions.unshift("user_id=?");
5548
+ args.unshift(userId);
5549
+ }
5550
+ if (range) {
5551
+ conditions.unshift("m.rowid>? AND m.rowid<=?");
5552
+ args.unshift(range.after, range.through);
5553
+ }
5554
+ return this._db.prepare(`DELETE FROM cloud_resource_metadata AS m WHERE ${conditions.join(" AND ")}
5555
+ AND ${exists.map((sql) => `NOT EXISTS (${sql})`).join(" AND ")}`).bind(...args);
5556
+ });
5428
5557
  const results = await this._cloudBatch(statements, userId);
5429
5558
  return results.reduce((sum, result) => sum + (result.meta.changes || 0), 0);
5430
5559
  }
@@ -5564,6 +5693,7 @@ var D1Adapter = class {
5564
5693
  indexResults.push({ name: index.name, status: "failed", description: index.description, critical: !!index.critical, error: error.message });
5565
5694
  }
5566
5695
  }
5696
+ for (const trigger of CLOUD_DATA_DELETE_TRIGGERS) await this._db.prepare(trigger.sql).run();
5567
5697
  const criticalFailures = indexResults.filter((i) => i.critical && i.status === "failed");
5568
5698
  if (criticalFailures.length > 0) {
5569
5699
  const names = criticalFailures.map((i) => i.name).join(", ");
@@ -5587,7 +5717,7 @@ var D1Adapter = class {
5587
5717
  * 构变了而老部署没跑过 initSchema 时,cron 会每分钟静默挂在缺的那一列上,
5588
5718
  * 界面上一切正常——这个方法就是让宿主查得出来。
5589
5719
  *
5590
- * @returns {Promise<{ tables: Record<string, string[]>, indexes: string[] }>}
5720
+ * @returns {Promise<{ tables: Record<string, string[]>, indexes: string[], triggers: string[] }>}
5591
5721
  */
5592
5722
  async describeSchema() {
5593
5723
  const tableRes = await this._db.prepare(
@@ -5604,7 +5734,8 @@ var D1Adapter = class {
5604
5734
  const indexRes = await this._db.prepare(
5605
5735
  `SELECT name FROM sqlite_master WHERE type = 'index' AND name IS NOT NULL`
5606
5736
  ).all();
5607
- return { tables, indexes: (indexRes.results || []).map((row) => row.name) };
5737
+ const triggerRes = await this._db.prepare("SELECT name FROM sqlite_master WHERE type = 'trigger'").all();
5738
+ return { tables, indexes: (indexRes.results || []).map((row) => row.name), triggers: (triggerRes.results || []).map((row) => row.name) };
5608
5739
  }
5609
5740
  async dropSchema() {
5610
5741
  await this._db.prepare("DROP TABLE IF EXISTS scheduled_messages").run();
@@ -5612,7 +5743,7 @@ var D1Adapter = class {
5612
5743
  await this._db.prepare("DROP TABLE IF EXISTS push_subscriptions").run();
5613
5744
  await this._db.prepare("DROP TABLE IF EXISTS llm_credentials").run();
5614
5745
  await this._db.prepare("DROP TABLE IF EXISTS message_outbox").run();
5615
- for (const table of ["cloud_data_record_chunks", "cloud_data_records", "cloud_data_owners", "cloud_resource_metadata", "cloud_guard_assertions"]) {
5746
+ for (const table of ["cloud_data_work", "cloud_data_maintenance", "cloud_data_record_chunks", "cloud_data_records", "cloud_data_owners", "cloud_resource_metadata", "cloud_guard_assertions"]) {
5616
5747
  await this._db.prepare(`DROP TABLE IF EXISTS ${table}`).run();
5617
5748
  }
5618
5749
  }
@@ -5890,7 +6021,8 @@ var D1Adapter = class {
5890
6021
  const cutoff = new Date(Date.now() - safeDays * 24 * 60 * 60 * 1e3).toISOString();
5891
6022
  const res = await this._db.prepare(
5892
6023
  `DELETE FROM scheduled_messages
5893
- WHERE status IN ('sent', 'failed') AND updated_at < ?`
6024
+ WHERE rowid IN (SELECT rowid FROM scheduled_messages
6025
+ WHERE status IN ('sent', 'failed') AND updated_at < ? LIMIT 100)`
5894
6026
  ).bind(cutoff).run();
5895
6027
  return res.meta.changes || 0;
5896
6028
  }
@@ -6056,7 +6188,7 @@ var D1Adapter = class {
6056
6188
  */
6057
6189
  async cleanupClientState(targets = []) {
6058
6190
  if (!Array.isArray(targets) || targets.length === 0) return 0;
6059
- const SQL = "DELETE FROM client_state WHERE namespace = ? AND updated_at < ?";
6191
+ const SQL = "DELETE FROM client_state WHERE rowid IN (SELECT rowid FROM client_state WHERE namespace = ? AND updated_at < ? LIMIT 100)";
6060
6192
  const statements = targets.map(
6061
6193
  (target) => this._db.prepare(SQL).bind(target.namespace, target.updatedBefore)
6062
6194
  );
@@ -6552,13 +6684,13 @@ var D1Adapter = class {
6552
6684
  let deleted = 0;
6553
6685
  if (Number.isFinite(ackedBeforeMs)) {
6554
6686
  const res = await this._db.prepare(
6555
- "DELETE FROM message_outbox WHERE acked_at IS NOT NULL AND acked_at < ?"
6687
+ "DELETE FROM message_outbox WHERE rowid IN (SELECT rowid FROM message_outbox WHERE acked_at IS NOT NULL AND acked_at < ? LIMIT 100)"
6556
6688
  ).bind(ackedBeforeMs).run();
6557
6689
  deleted += res.meta.changes || 0;
6558
6690
  }
6559
6691
  if (Number.isFinite(allBeforeMs)) {
6560
6692
  const res = await this._db.prepare(
6561
- "DELETE FROM message_outbox WHERE created_at < ?"
6693
+ "DELETE FROM message_outbox WHERE rowid IN (SELECT rowid FROM message_outbox WHERE created_at < ? LIMIT 100)"
6562
6694
  ).bind(allBeforeMs).run();
6563
6695
  deleted += res.meta.changes || 0;
6564
6696
  }
@@ -6879,7 +7011,7 @@ function createClientStateNamespacesHandler(ctx) {
6879
7011
  }
6880
7012
 
6881
7013
  // src/server/lib/version.js
6882
- var SERVER_VERSION = true ? "2.6.0-next.34" : "0.0.0-dev";
7014
+ var SERVER_VERSION = true ? "2.6.0-next.35" : "0.0.0-dev";
6883
7015
 
6884
7016
  // src/server/handlers/capabilities.js
6885
7017
  var SERVER_FEATURES = Object.freeze([
@@ -7301,6 +7433,15 @@ async function cloudInventory(ctx, db, userId, userKey) {
7301
7433
  resolveOwner: ctx.cloudData?.resolveOwner
7302
7434
  });
7303
7435
  }
7436
+ function operationWork(operation) {
7437
+ if (["pending", "running"].includes(operation.status)) {
7438
+ return { nextRunAt: Number(operation.nextAttemptAt) || 0, expiresAt: null };
7439
+ }
7440
+ if (["completed", "failed"].includes(operation.status)) {
7441
+ return { nextRunAt: null, expiresAt: Number(operation.updatedAt) + 30 * 864e5 };
7442
+ }
7443
+ return { nextRunAt: null, expiresAt: null };
7444
+ }
7304
7445
  async function save(db, userId, key, kind, value, options) {
7305
7446
  return db.putCloudDataRecord(
7306
7447
  userId,
@@ -7313,7 +7454,7 @@ async function save(db, userId, key, kind, value, options) {
7313
7454
  }),
7314
7455
  key
7315
7456
  ),
7316
- options
7457
+ { ...options, ...kind === "operation" ? { work: operationWork(value) } : {} }
7317
7458
  );
7318
7459
  }
7319
7460
  async function readCloudRecord(db, userId, key, kind, id) {
@@ -7675,25 +7816,44 @@ async function advanceCleanup(ctx, db, userId, key, operation) {
7675
7816
  }
7676
7817
  return operation;
7677
7818
  }
7819
+ async function repairLegacyWorkIndex(ctx) {
7820
+ const db = ctx.db;
7821
+ if (typeof db.getCloudMaintenanceBatch !== "function") return;
7822
+ const name = "management-work-v1";
7823
+ const batch = await db.getCloudMaintenanceBatch(name, "cloud_data_records", 100);
7824
+ if (batch.completed) return;
7825
+ for (const row of batch.rows) {
7826
+ let work;
7827
+ if (["inventory", "plan"].includes(row.kind)) {
7828
+ work = { nextRunAt: null, expiresAt: row.updated_at + 36e5 };
7829
+ } else if (row.kind === "operation") {
7830
+ try {
7831
+ const record = await db.readCloudMaintenanceRecord(row);
7832
+ const key = await deriveUserEncryptionKey(row.user_id, ctx.masterKey);
7833
+ work = operationWork(JSON.parse(await decryptFromStorage(record.data, key)));
7834
+ } catch {
7835
+ console.warn("[amsg] Legacy cleanup operation could not be indexed");
7836
+ continue;
7837
+ }
7838
+ } else continue;
7839
+ await db.indexCloudDataRecordWork(row.user_id, row.kind, row.id, work, row.write_token);
7840
+ }
7841
+ const cursor = batch.rows.at(-1)?.maintenance_rowid ?? batch.upperBound;
7842
+ await db.finishCloudMaintenanceBatch(name, cursor, batch.rows.length < 100 || cursor >= batch.upperBound);
7843
+ }
7678
7844
  async function resumeCloudDataCleanups(ctx) {
7679
7845
  const db = ctx.db;
7680
7846
  if (!db?.cloudDataManagement) return;
7681
- const rows = await db.listCloudDataRecordsAcrossUsers("operation");
7682
- await db.cleanupCloudDataRecords("inventory", Date.now() - 36e5);
7683
- await db.cleanupCloudDataRecords("plan", Date.now() - 36e5);
7684
- await db.cleanupCloudResourceMetadata();
7685
- let processed = 0;
7847
+ await repairLegacyWorkIndex(ctx);
7848
+ if (typeof db.repairCloudResourceMetadata === "function") await db.repairCloudResourceMetadata(100);
7849
+ if (typeof db.cleanupExpiredCloudDataRecords === "function") await db.cleanupExpiredCloudDataRecords(Date.now(), 100);
7850
+ const rows = typeof db.listDueCloudDataOperations === "function" ? await db.listDueCloudDataOperations(Date.now(), 25) : await db.listCloudDataRecordsAcrossUsers("operation");
7686
7851
  for (const row of rows) {
7687
7852
  try {
7688
7853
  const key = await deriveUserEncryptionKey(row.userId, ctx.masterKey);
7689
7854
  const operation = JSON.parse(await decryptFromStorage(row.data, key));
7690
- if (["completed", "failed"].includes(operation.status) && operation.updatedAt < Date.now() - 30 * 864e5) {
7691
- await db.deleteCloudDataRecord(row.userId, "operation", operation.id);
7692
- continue;
7693
- }
7694
7855
  if (["pending", "running"].includes(operation.status)) {
7695
7856
  await advanceCleanup(ctx, db, row.userId, key, operation);
7696
- if (++processed >= 100) break;
7697
7857
  }
7698
7858
  } catch {
7699
7859
  console.warn("[amsg] A cloud cleanup operation could not be resumed");
@@ -7702,6 +7862,19 @@ async function resumeCloudDataCleanups(ctx) {
7702
7862
  }
7703
7863
 
7704
7864
  // src/server/handlers/cloud-data.js
7865
+ function inventorySummary(inventory) {
7866
+ const resources = inventory.entries.map((entry) => entry.resource);
7867
+ return {
7868
+ total: resources.length,
7869
+ counts: CLOUD_RESOURCE_TYPES.map((type) => ({
7870
+ type,
7871
+ count: resources.filter((resource) => resource.type === type).length,
7872
+ byteSize: resources.filter((resource) => resource.type === type).reduce((sum, resource) => sum + (resource.byteSize || 0), 0)
7873
+ })),
7874
+ complete: inventory.complete,
7875
+ gaps: inventory.gaps
7876
+ };
7877
+ }
7705
7878
  function createCloudDataHandler(ctx) {
7706
7879
  async function run(url, headers, body, method) {
7707
7880
  const tenant = await ctx.tenantManager.resolveTenant(headers);
@@ -7748,17 +7921,7 @@ function createCloudDataHandler(ctx) {
7748
7921
  const filter = { ownerType, ownerId, type };
7749
7922
  if (path.endsWith("/summary")) {
7750
7923
  const inventory = await cloudInventory(ctx, db, userId, key);
7751
- const resources = inventory.entries.map((e) => e.resource);
7752
- data = {
7753
- total: resources.length,
7754
- counts: CLOUD_RESOURCE_TYPES.map((type2) => ({
7755
- type: type2,
7756
- count: resources.filter((r) => r.type === type2).length,
7757
- byteSize: resources.filter((r) => r.type === type2).reduce((sum, r) => sum + (r.byteSize || 0), 0)
7758
- })),
7759
- complete: inventory.complete,
7760
- gaps: inventory.gaps
7761
- };
7924
+ data = inventorySummary(inventory);
7762
7925
  } else {
7763
7926
  const limit = Number(params.get("limit") || 50);
7764
7927
  if (!Number.isInteger(limit) || limit < 1 || limit > 200)
@@ -7790,6 +7953,7 @@ function createCloudDataHandler(ctx) {
7790
7953
  snapshot = {
7791
7954
  id: crypto.randomUUID(),
7792
7955
  filter,
7956
+ summary: inventorySummary(inventory),
7793
7957
  expiresAt: Date.now() + 15 * 60 * 1e3,
7794
7958
  resources: inventory.entries.map((e) => e.resource).filter(
7795
7959
  (r) => (!type || r.type === type) && (!ownerId || r.owner?.type === ownerType && r.owner.id === ownerId)
@@ -7806,6 +7970,7 @@ function createCloudDataHandler(ctx) {
7806
7970
  }
7807
7971
  data = {
7808
7972
  resources: snapshot.resources.slice(offset, offset + limit),
7973
+ ...snapshot.summary ? { summary: snapshot.summary } : {},
7809
7974
  nextCursor: offset + limit < snapshot.resources.length ? `${snapshot.id}:${offset + limit}` : null,
7810
7975
  complete: snapshot.complete,
7811
7976
  gaps: snapshot.gaps
@@ -8150,7 +8315,7 @@ function createSingleUserServer(config) {
8150
8315
  }
8151
8316
 
8152
8317
  // src/server/lib/schema-version.js
8153
- var SCHEMA_VERSION = "2.6.0-cloud-data.1";
8318
+ var SCHEMA_VERSION = "2.6.0-cloud-data.2";
8154
8319
  function requireIntrospection(db) {
8155
8320
  if (!db || typeof db.describeSchema !== "function") {
8156
8321
  throw new Error(
@@ -8178,6 +8343,10 @@ async function getSchemaVersion(db) {
8178
8343
  for (const index of SQLITE_REQUIRED_SCHEMA.indexes) {
8179
8344
  if (!liveIndexes.has(index)) missing.push(`index:${index}`);
8180
8345
  }
8346
+ const liveTriggers = new Set(live?.triggers || []);
8347
+ for (const name of SQLITE_REQUIRED_SCHEMA.triggers) {
8348
+ if (!liveTriggers.has(name)) missing.push(`trigger:${name}`);
8349
+ }
8181
8350
  const ok = missing.length === 0;
8182
8351
  return { current: ok ? SCHEMA_VERSION : null, required: SCHEMA_VERSION, ok, missing };
8183
8352
  }