@relaycast/engine 8.5.2 → 8.5.3

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.
Files changed (44) hide show
  1. package/dist/db/migrations/0048_bounded_maintenance.sql +20 -0
  2. package/dist/db/migrations/0049_redrive_review_hardening.sql +11 -0
  3. package/dist/db/schema.d.ts +46 -0
  4. package/dist/db/schema.d.ts.map +1 -1
  5. package/dist/db/schema.js +26 -0
  6. package/dist/db/schema.js.map +1 -1
  7. package/dist/engine/action.d.ts.map +1 -1
  8. package/dist/engine/action.js +2 -1
  9. package/dist/engine/action.js.map +1 -1
  10. package/dist/engine/boundedRetention.d.ts +13 -0
  11. package/dist/engine/boundedRetention.d.ts.map +1 -0
  12. package/dist/engine/boundedRetention.js +204 -0
  13. package/dist/engine/boundedRetention.js.map +1 -0
  14. package/dist/engine/delivery.d.ts +5 -0
  15. package/dist/engine/delivery.d.ts.map +1 -1
  16. package/dist/engine/delivery.js +154 -96
  17. package/dist/engine/delivery.js.map +1 -1
  18. package/dist/engine/deliveryWrites.d.ts.map +1 -1
  19. package/dist/engine/deliveryWrites.js +15 -6
  20. package/dist/engine/deliveryWrites.js.map +1 -1
  21. package/dist/engine/invocationRetry.d.ts +3 -0
  22. package/dist/engine/invocationRetry.d.ts.map +1 -0
  23. package/dist/engine/invocationRetry.js +5 -0
  24. package/dist/engine/invocationRetry.js.map +1 -0
  25. package/dist/engine/nodeRedriveCandidates.d.ts +15 -0
  26. package/dist/engine/nodeRedriveCandidates.d.ts.map +1 -0
  27. package/dist/engine/nodeRedriveCandidates.js +72 -0
  28. package/dist/engine/nodeRedriveCandidates.js.map +1 -0
  29. package/dist/engine/retention.d.ts +5 -36
  30. package/dist/engine/retention.d.ts.map +1 -1
  31. package/dist/engine/retention.js +6 -197
  32. package/dist/engine/retention.js.map +1 -1
  33. package/dist/lib/settlePool.d.ts +3 -0
  34. package/dist/lib/settlePool.d.ts.map +1 -0
  35. package/dist/lib/settlePool.js +18 -0
  36. package/dist/lib/settlePool.js.map +1 -0
  37. package/dist/lib/singleFlight.d.ts +6 -0
  38. package/dist/lib/singleFlight.d.ts.map +1 -0
  39. package/dist/lib/singleFlight.js +36 -0
  40. package/dist/lib/singleFlight.js.map +1 -0
  41. package/dist/routes/deliveryRouting.d.ts.map +1 -1
  42. package/dist/routes/deliveryRouting.js +8 -4
  43. package/dist/routes/deliveryRouting.js.map +1 -1
  44. package/package.json +3 -3
@@ -0,0 +1,204 @@
1
+ import { sql } from 'drizzle-orm';
2
+ import { z } from 'zod';
3
+ import { snowflakeIdLowerBound } from './snowflake.js';
4
+ const DAY_MS = 86_400_000;
5
+ const position = z.array(z.union([z.string(), z.number()]));
6
+ const stateSchema = z.object({
7
+ next: z.number().int().min(0).max(4),
8
+ positions: z.record(z.string(), position),
9
+ highs: z.record(z.string(), position).default({}),
10
+ });
11
+ const tables = [
12
+ { result: 'messages', name: 'messages', index: 'idx_messages_retention',
13
+ keys: ['length(id)', 'id'], setting: 'message_ttl_days', fallback: 'messageTtlDays',
14
+ eligible: 'NOT EXISTS (SELECT 1 FROM messages replies WHERE replies.thread_id = page.id)' },
15
+ { result: 'deliveries', name: 'deliveries', index: 'idx_deliveries_settled_retention',
16
+ keys: ['created_at', 'id'], setting: 'delivery_ttl_days', fallback: 'deliveryTtlDays',
17
+ predicate: "status IN ('acked', 'failed', 'dead_lettered')" },
18
+ { result: 'messageLogs', name: 'message_logs', index: 'idx_message_logs_retention',
19
+ keys: ['length(id)', 'id'], setting: 'message_log_ttl_days', fallback: 'messageLogTtlDays' },
20
+ { result: 'workspaceEvents', name: 'workspace_events', index: 'idx_workspace_events_retention',
21
+ keys: ['created_at', 'workspace_id', 'seq'], setting: 'workspace_event_ttl_days',
22
+ fallback: 'workspaceEventTtlDays',
23
+ eligible: 'EXISTS (SELECT 1 FROM workspace_events hw WHERE hw.workspace_id = page.workspace_id AND hw.seq > page.seq LIMIT 1)' },
24
+ { result: 'readReceipts', name: 'read_receipts', index: 'idx_read_receipts_retention',
25
+ keys: ['message_id', 'agent_id'],
26
+ eligible: 'NOT EXISTS (SELECT 1 FROM messages m WHERE m.id = page.message_id)' },
27
+ ];
28
+ /** Project a candidate onto its table's ordered keyset cursor. */
29
+ function key(row, table) {
30
+ return table.keys.map(k => k === 'length(id)' ? row.id.length : row[k]);
31
+ }
32
+ /** Apply exact workspace policy after the bounded candidate read. */
33
+ function expired(row, table, defaults, nowMs) {
34
+ if (!row.eligible)
35
+ return false;
36
+ if (!table.setting || !table.fallback)
37
+ return true;
38
+ const settings = row.retention ? JSON.parse(row.retention) : {};
39
+ const override = settings[table.setting];
40
+ const ttl = override === undefined ? defaults[table.fallback] : override;
41
+ if (ttl == null || !Number.isFinite(ttl) || ttl <= 0)
42
+ return false;
43
+ const cutoff = nowMs - ttl * DAY_MS;
44
+ if (table.keys[0] === 'length(id)') {
45
+ const bound = snowflakeIdLowerBound(cutoff);
46
+ return row.id.length < bound.length || (row.id.length === bound.length && row.id < bound);
47
+ }
48
+ return row.created_at * 1000 < cutoff;
49
+ }
50
+ /** Construct a bound primary-key predicate without interpolating user SQL. */
51
+ function identity(row, table) {
52
+ if (table.result === 'workspaceEvents')
53
+ return sql `(workspace_id = ${row.workspace_id} AND seq = ${row.seq})`;
54
+ if (table.result === 'readReceipts')
55
+ return sql `(message_id = ${row.message_id} AND agent_id = ${row.agent_id})`;
56
+ return sql `id = ${row.id}`;
57
+ }
58
+ /**
59
+ * Bound candidate reads, not just returned/deleted rows. Persist progress even
60
+ * when a page contains only retained rows, so disabled TTLs, live receipts and
61
+ * event high-water marks cannot pin a scan to the beginning forever.
62
+ *
63
+ * Each table completes at most one traversal per run; cursors wrap at EOF.
64
+ * Cursor commits follow successful deletion. A crash can replay a page (safe),
65
+ * never skip an uncommitted deletion. Hosts should serialize maintenance runs.
66
+ */
67
+ export async function pruneBounded(db, opts) {
68
+ const limit = boundedInteger(opts.batchLimit, 200, 200);
69
+ const rounds = boundedInteger(opts.maxBatches, 5, 5);
70
+ const started = Date.now();
71
+ const budget = boundedInteger(opts.maxDurationMs, 10_000, 30_000);
72
+ const nowMs = (opts.now ?? new Date()).getTime();
73
+ const defaults = {
74
+ messageTtlDays: null, deliveryTtlDays: 90, messageLogTtlDays: 90, workspaceEventTtlDays: 30,
75
+ ...Object.fromEntries(Object.entries(opts.defaults ?? {}).filter(([, value]) => value !== undefined)),
76
+ };
77
+ // One small policy aggregate (no workspace-ID bind list), not one history
78
+ // scan per workspace. The shortest positive TTL provides a safe global index
79
+ // cutoff; exact per-workspace policy is still checked on each candidate.
80
+ // This avoids walking millions of recent rows that no policy can expire.
81
+ const ttlTables = tables.filter(table => table.setting);
82
+ const [minimums] = await db.all(sql `SELECT
83
+ ${sql.join(ttlTables.map(table => sql.raw(`MIN(CASE WHEN json_extract(retention, '$.${table.setting}') > 0 THEN json_extract(retention, '$.${table.setting}') END) AS "${table.result}"`)), sql `, `)} FROM workspaces`);
84
+ if (!Number.isFinite(nowMs) && [
85
+ ...Object.values(defaults), ...Object.values(minimums ?? {}),
86
+ ].some(ttl => typeof ttl === 'number' && Number.isFinite(ttl) && ttl > 0)) {
87
+ throw new Error('Invalid retention clock: expected a finite Date when a positive TTL is active');
88
+ }
89
+ const saved = await db.all(sql `SELECT cursor FROM maintenance_cursors WHERE id = 'retention-v1'`);
90
+ const parsed = saved[0] ? stateSchema.safeParse(decodeCursor(saved[0].cursor)) : undefined;
91
+ const state = parsed?.success ? parsed.data : { next: 0, positions: {}, highs: {} };
92
+ const result = { messages: 0, deliveries: 0, messageLogs: 0, readReceipts: 0, workspaceEvents: 0 };
93
+ const finished = new Set();
94
+ for (let step = 0; step < rounds * tables.length && Date.now() - started < budget; step++) {
95
+ const index = state.next;
96
+ state.next = (index + 1) % tables.length;
97
+ if (finished.has(index))
98
+ continue;
99
+ const table = tables[index];
100
+ const cursor = state.positions[table.name];
101
+ const positiveTtls = table.fallback
102
+ ? [defaults[table.fallback], minimums?.[table.result]].filter((ttl) => typeof ttl === 'number' && Number.isFinite(ttl) && ttl > 0)
103
+ : [];
104
+ if (table.fallback && !positiveTtls.length) {
105
+ delete state.positions[table.name];
106
+ delete state.highs[table.name];
107
+ finished.add(index);
108
+ await db.run(sql `INSERT INTO maintenance_cursors (id, cursor) VALUES ('retention-v1', ${JSON.stringify(state)})
109
+ ON CONFLICT(id) DO UPDATE SET cursor = excluded.cursor`);
110
+ continue;
111
+ }
112
+ const columns = sql.raw(table.result === 'readReceipts' ? 'message_id, agent_id'
113
+ : table.result === 'workspaceEvents' ? 'workspace_id, seq, created_at'
114
+ : 'id, workspace_id, created_at');
115
+ // A traversal has a finite end even while new rows keep arriving. Without
116
+ // this fence the cursor might never wrap to retained rows that later age out.
117
+ if (!state.highs[table.name]) {
118
+ const [last] = await db.all(sql `SELECT ${columns}
119
+ FROM ${sql.raw(table.name)} INDEXED BY ${sql.raw(table.index)}
120
+ ${table.predicate ? sql `WHERE ${sql.raw(table.predicate)}` : sql ``}
121
+ ORDER BY ${sql.raw(table.keys.map(k => k + ' DESC').join(', '))} LIMIT 1`);
122
+ if (last)
123
+ state.highs[table.name] = key(last, table);
124
+ }
125
+ const predicates = [];
126
+ if (table.predicate)
127
+ predicates.push(sql.raw(table.predicate));
128
+ if (table.fallback) {
129
+ const cutoff = nowMs - Math.min(...positiveTtls) * DAY_MS;
130
+ if (table.keys[0] === 'length(id)') {
131
+ const bound = snowflakeIdLowerBound(cutoff);
132
+ predicates.push(sql `(length(id), id) < (${bound.length}, ${bound})`);
133
+ }
134
+ else {
135
+ predicates.push(sql `created_at < ${Math.ceil(cutoff / 1000)}`);
136
+ }
137
+ }
138
+ if (cursor?.length === table.keys.length) {
139
+ predicates.push(sql `(${sql.raw(table.keys.join(', '))}) > (${sql.join(cursor.map(v => sql `${v}`), sql `, `)})`);
140
+ }
141
+ const high = state.highs[table.name];
142
+ if (high?.length === table.keys.length) {
143
+ predicates.push(sql `(${sql.raw(table.keys.join(', '))}) <= (${sql.join(high.map(v => sql `${v}`), sql `, `)})`);
144
+ }
145
+ // MATERIALIZED prevents the planner pushing eligibility/TTL checks ahead
146
+ // of LIMIT. The keyset seek and covering index bound candidate traversal.
147
+ const page = await db.all(sql `
148
+ WITH page AS MATERIALIZED (
149
+ SELECT ${columns}
150
+ FROM ${sql.raw(table.name)} INDEXED BY ${sql.raw(table.index)}
151
+ ${predicates.length ? sql `WHERE ${sql.join(predicates, sql ` AND `)}` : sql ``}
152
+ ORDER BY ${sql.raw(table.keys.join(', '))} LIMIT ${limit}
153
+ )
154
+ SELECT page.*, ${sql.raw(table.eligible ?? '1')} AS eligible,
155
+ ${table.setting ? sql `w.retention` : sql `NULL`} AS retention
156
+ FROM page
157
+ ${table.setting ? sql `LEFT JOIN workspaces w ON w.id = page.workspace_id` : sql ``}
158
+ ORDER BY ${sql.raw(table.keys.map(k => k === 'length(id)' ? 'length(page.id)' : 'page.' + k).join(', '))}
159
+ `);
160
+ const removable = page.filter(row => expired(row, table, defaults, nowMs));
161
+ // Two-key identities need two bindings each; leave room under D1's 100.
162
+ for (let offset = 0; offset < removable.length; offset += 40) {
163
+ const chunk = removable.slice(offset, offset + 40);
164
+ const guard = table.result === 'messages'
165
+ ? sql `AND NOT EXISTS (SELECT 1 FROM messages replies WHERE replies.thread_id = messages.id)`
166
+ : table.result === 'readReceipts'
167
+ ? sql `AND NOT EXISTS (SELECT 1 FROM messages m WHERE m.id = read_receipts.message_id)`
168
+ : table.result === 'workspaceEvents'
169
+ ? sql `AND EXISTS (SELECT 1 FROM workspace_events hw WHERE hw.workspace_id = workspace_events.workspace_id AND hw.seq > workspace_events.seq LIMIT 1)`
170
+ : table.predicate ? sql `AND ${sql.raw(table.predicate)}` : sql ``;
171
+ const deleted = await db.all(sql `DELETE FROM ${sql.raw(table.name)}
172
+ WHERE (${sql.join(chunk.map(row => identity(row, table)), sql ` OR `)}) ${guard}
173
+ RETURNING 1`);
174
+ result[table.result] += deleted.length;
175
+ }
176
+ if (page.length < limit || JSON.stringify(key(page[page.length - 1], table)) === JSON.stringify(high)) {
177
+ delete state.positions[table.name];
178
+ delete state.highs[table.name];
179
+ finished.add(index);
180
+ }
181
+ else {
182
+ state.positions[table.name] = key(page[page.length - 1], table);
183
+ }
184
+ await db.run(sql `INSERT INTO maintenance_cursors (id, cursor) VALUES ('retention-v1', ${JSON.stringify(state)})
185
+ ON CONFLICT(id) DO UPDATE SET cursor = excluded.cursor`);
186
+ if (finished.size === tables.length)
187
+ break;
188
+ }
189
+ return result;
190
+ }
191
+ /** Decode advisory scan state; corrupt text restarts a safe traversal. */
192
+ function decodeCursor(raw) {
193
+ try {
194
+ return JSON.parse(raw);
195
+ }
196
+ catch {
197
+ return undefined;
198
+ }
199
+ }
200
+ /** Clamp caller tuning to the supported finite maintenance budget. */
201
+ function boundedInteger(value, fallback, maximum) {
202
+ return value !== undefined && Number.isFinite(value) ? Math.max(1, Math.min(maximum, Math.floor(value))) : fallback;
203
+ }
204
+ //# sourceMappingURL=boundedRetention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boundedRetention.js","sourceRoot":"","sources":["../../src/engine/boundedRetention.ts"],"names":[],"mappings":"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"}
@@ -132,6 +132,7 @@ export declare function ackDeliveriesUpToSeq(db: Db, workspaceId: string, nodeId
132
132
  up_to_seq: number;
133
133
  acked: number;
134
134
  } | null>;
135
+ /** Mark still-queued rows delivered using bounded, explicit ID-index writes. */
135
136
  export declare function markDeliveriesDelivered(db: Db, workspaceId: string, deliveryIds: string[]): Promise<number>;
136
137
  export interface DeliveryFailureNotice {
137
138
  workspace_id: string;
@@ -157,6 +158,7 @@ export interface ExpiredDeliveryBatch {
157
158
  */
158
159
  export declare function expireDueDeliveryBatch(db: Db, workspaceId: string | undefined, now?: Date): Promise<ExpiredDeliveryBatch>;
159
160
  export declare function expireDueDeliveries(db: Db, workspaceId: string | undefined, now?: Date): Promise<DeliveryFailureNotice[]>;
161
+ /** Hydrate bounded resumable redrive windows; empty windows resume next sweep. */
160
162
  export declare function fetchDueNodeDeliveryEvents(db: Db, opts?: {
161
163
  workspaceId?: string;
162
164
  now?: Date;
@@ -188,6 +190,9 @@ export interface NodeDeliveryReplayScope {
188
190
  /** Limit replay to identities whose broker-side cursor is ready. */
189
191
  agentIds?: readonly string[];
190
192
  }
193
+ /** Coalesce identical triggers at the socket owner, including a trailing replay
194
+ * when readiness/cursors changed during an outstanding drain. This is local
195
+ * replay coordination, not a global database admission semaphore. */
191
196
  export declare function deliverPendingToNode(db: Db, registry: NodeConnectionRegistry, workspaceId: string, nodeId: string, scope?: NodeDeliveryReplayScope): Promise<number>;
192
197
  export {};
193
198
  //# sourceMappingURL=delivery.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"delivery.d.ts","sourceRoot":"","sources":["../../src/engine/delivery.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"delivery.d.ts","sourceRoot":"","sources":["../../src/engine/delivery.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,UAAU,EAA6E,MAAM,iBAAiB,CAAC;AACxH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAQnE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAEhE,KAAK,EAAE,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AAEnC,KAAK,WAAW,GAAG,OAAO,UAAU,CAAC,YAAY,CAAC;AAwBlD,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAOD,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C,iBAAS,iBAAiB,CAAC,GAAG,EAAE,WAAW,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;YAM1C,cAAc;;;;;;;;;;;;;;;;;;;;;;;;EAyBvC;AAGD;;;;;GAKG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,cAAc,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO;;;;;;;;;;;;;;YAtC9B,cAAc;;;;;;;;;;;;;;;;;;;;;;;;KAwGvC;AA6BD,MAAM,MAAM,gBAAgB,GAAG;IAAE,QAAQ,EAAE,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC;AAEpG;;;;GAIG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAYlC;AAED;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GACjD,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAsBlC;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE;IAAE,WAAW,EAAE,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3C,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAsClC;AAsED,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAqC5F;AAED,gFAAgF;AAChF,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,MAAM,CAAC,CAoBjB;AAED,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,KAAK,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,qBAAqB,EAAE,CAAC;CAClC;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,GAAG,GAAE,IAAiB,GACrB,OAAO,CAAC,oBAAoB,CAAC,CAyD/B;AAED,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,GAAG,GAAE,IAAiB,GACrB,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAElC;AA4ID,kFAAkF;AAClF,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,EAAE,EACN,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAC9D,OAAO,CAAC,qBAAqB,EAAE,CAAC,CA+GlC;AAED;;;GAGG;AACH,eAAO,MAAM,8BAA8B,mCAA6B,CAAC;AAOzE;;;;;;;;;;GAUG;AACH,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,EAAE,EACN,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,GAAG,CAAC,EAAE,IAAI,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GACxC,OAAO,CAAC,qBAAqB,EAAE,CAAC,CAsDlC;AAED,MAAM,WAAW,uBAAuB;IACtC,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAID;;qEAEqE;AACrE,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,EAAE,EACN,QAAQ,EAAE,sBAAsB,EAChC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,uBAA4B,GAClC,OAAO,CAAC,MAAM,CAAC,CAMjB"}
@@ -1,11 +1,16 @@
1
- import { eq, and, not, asc, isNull, inArray, notInArray, lte, gt, sql } from 'drizzle-orm';
1
+ import { eq, and, not, asc, isNull, inArray, notInArray, lte, gt, sql, getTableColumns } from 'drizzle-orm';
2
2
  import { deliveries, messages, agents, readReceipts, channelMembers, channels, dmConversations } from '../db/schema.js';
3
3
  import { runAtomic } from '../ports/database.js';
4
4
  import { isProviderAgentDeliveryReady } from '../ports/realtime.js';
5
5
  import { buildDeliverFrame, buildDeliverPayload, buildMessageCreatedEventData, buildThreadReplyEventData, buildDmReceivedEventData, buildGroupDmReceivedEventData } from './deliveryWire.js';
6
6
  import { publicMessageMetadata } from './messageMetadata.js';
7
7
  import { toIso } from '../lib/serialize.js';
8
+ import { trailingSingleFlight } from '../lib/singleFlight.js';
9
+ import { readNodeRedriveCandidates } from './nodeRedriveCandidates.js';
8
10
  import { fetchAttachmentsBatch } from './attachments.js';
11
+ // SQL-wrapped columns retain Drizzle's timestamp/boolean decoders when FROM
12
+ // includes an explicit SQLite INDEXED BY clause rather than a table object.
13
+ const indexedDeliveryColumns = Object.fromEntries(Object.entries(getTableColumns(deliveries)).map(([name, column]) => [name, sql `${column}`.mapWith(column)]));
9
14
  const ACTIVE_DELIVERY_STATUSES = ['queued', 'delivered'];
10
15
  const TERMINAL_SUCCESS_STATUS = 'acked';
11
16
  // Keep expiry work bounded per request and leave ample room under D1's
@@ -276,21 +281,27 @@ export async function ackDeliveriesUpToSeq(db, workspaceId, nodeId, providerName
276
281
  const updated = await ackRows(db, workspaceId, agent.id, rows.map((row) => ({ ...row.delivery, channelId: row.channelId })), upToSeq);
277
282
  return { agent_id: agent.id, agent_name: agent.name, up_to_seq: upToSeq, acked: updated.length };
278
283
  }
284
+ /** Mark still-queued rows delivered using bounded, explicit ID-index writes. */
279
285
  export async function markDeliveriesDelivered(db, workspaceId, deliveryIds) {
280
- if (deliveryIds.length === 0)
281
- return 0;
282
- const updated = await db
283
- .update(deliveries)
284
- .set({
285
- status: 'delivered',
286
- nextAttemptAt: null,
287
- lastDispatchError: null,
288
- deliveredAt: new Date(),
289
- updatedAt: new Date(),
290
- })
291
- .where(and(eq(deliveries.workspaceId, workspaceId), inArray(deliveries.id, deliveryIds), eq(deliveries.status, 'queued')))
292
- .returning();
293
- return updated.length;
286
+ let count = 0;
287
+ const ids = [...new Set(deliveryIds)];
288
+ const now = Math.floor(Date.now() / 1000);
289
+ // Force primary-key writes too: a bounded replay read is not enough if its
290
+ // status UPDATE chooses a workspace/status index and scans retained history.
291
+ for (let offset = 0; offset < ids.length; offset += 50) {
292
+ const page = ids.slice(offset, offset + 50);
293
+ const updated = await db.all(sql `
294
+ UPDATE deliveries INDEXED BY idx_deliveries_id_lookup
295
+ SET status = 'delivered', next_attempt_at = NULL, last_dispatch_error = NULL,
296
+ delivered_at = ${now}, updated_at = ${now}
297
+ WHERE workspace_id = ${workspaceId}
298
+ AND id IN (${sql.join(page.map(id => sql `${id}`), sql `, `)})
299
+ AND status = 'queued'
300
+ RETURNING id
301
+ `);
302
+ count += updated.length;
303
+ }
304
+ return count;
294
305
  }
295
306
  /**
296
307
  * Transition one D1-safe batch of due deliveries and report both the number of
@@ -477,10 +488,13 @@ function fanoutRecordFromDeliveryRow(row) {
477
488
  // re-attempts once the node is connected + delivery-ready). Mirrors
478
489
  // `WS_NODE_KINDS` in `nodeDeliver.ts`.
479
490
  const NODE_REDRIVE_KINDS = ['http_push', 'ws', 'fleet_ws', 'direct_ws'];
491
+ /** Hydrate bounded resumable redrive windows; empty windows resume next sweep. */
480
492
  export async function fetchDueNodeDeliveryEvents(db, opts = {}) {
481
- const limit = Math.min(Math.max(opts.limit ?? 50, 1), 200);
493
+ const limit = Number.isFinite(opts.limit) ? Math.min(Math.max(Math.floor(opts.limit), 1), 200) : 50;
482
494
  const now = opts.now ?? new Date();
483
495
  const nowSeconds = Math.floor(now.getTime() / 1000);
496
+ if (!Number.isFinite(nowSeconds))
497
+ throw new Error('Invalid node redrive clock: expected a finite Date');
484
498
  const conditions = [
485
499
  eq(deliveries.status, 'queued'),
486
500
  inArray(deliveries.routeNodeKind, [...NODE_REDRIVE_KINDS]),
@@ -489,9 +503,9 @@ export async function fetchDueNodeDeliveryEvents(db, opts = {}) {
489
503
  if (opts.workspaceId) {
490
504
  conditions.push(eq(deliveries.workspaceId, opts.workspaceId));
491
505
  }
492
- const fetchRows = (attemptCondition, queryLimit, orderByRetryAt) => db
506
+ const fetchRows = (ids) => db
493
507
  .select({
494
- delivery: deliveries,
508
+ delivery: indexedDeliveryColumns,
495
509
  recipientAgentName: agents.name,
496
510
  body: messages.body,
497
511
  blocks: messages.blocks,
@@ -508,30 +522,43 @@ export async function fetchDueNodeDeliveryEvents(db, opts = {}) {
508
522
  SELECT a.name FROM agents a WHERE a.id = ${messages.agentId}
509
523
  )`,
510
524
  })
511
- .from(deliveries)
525
+ .from(sql `${deliveries} INDEXED BY idx_deliveries_id_lookup`)
512
526
  .innerJoin(agents, eq(deliveries.agentId, agents.id))
513
527
  .innerJoin(messages, eq(deliveries.messageId, messages.id))
514
528
  .innerJoin(channels, eq(messages.channelId, channels.id))
515
529
  .leftJoin(dmConversations, eq(dmConversations.channelId, messages.channelId))
516
- .where(and(...conditions, attemptCondition))
517
- .orderBy(...(orderByRetryAt ? [asc(deliveries.nextAttemptAt)] : []), asc(deliveries.createdAt), asc(deliveries.id))
518
- .limit(queryLimit);
519
- // SQLite orders NULL before timestamps, so preserve the former OR query's
520
- // ordering by filling the page with never-attempted rows first. Splitting the
521
- // branches lets the due-retry half use idx_deliveries_retry_due; a partial
522
- // non-NULL index cannot serve an OR that also asks for NULL rows.
523
- const neverAttempted = await fetchRows(isNull(deliveries.nextAttemptAt), limit, false);
530
+ .where(and(...conditions, inArray(deliveries.id, ids), sql `(${deliveries.nextAttemptAt} IS NULL OR ${deliveries.nextAttemptAt} <= ${nowSeconds})`))
531
+ .orderBy(asc(deliveries.nextAttemptAt), asc(deliveries.createdAt), asc(deliveries.id));
532
+ // Scan at most one metadata window per lane. Expiry is checked AFTER LIMIT,
533
+ // and a durable cursor advances past excluded rows instead of repeatedly
534
+ // scanning the same prefix. Hydration rechecks mutable eligibility by ID.
535
+ const neverAttempted = await readNodeRedriveCandidates(db, { workspaceId: opts.workspaceId, retry: false, limit, nowSeconds });
524
536
  const dueRetries = neverAttempted.length < limit
525
- ? await fetchRows(sql `${deliveries.nextAttemptAt} IS NOT NULL AND ${deliveries.nextAttemptAt} <= ${nowSeconds}`, limit - neverAttempted.length, true)
537
+ ? await readNodeRedriveCandidates(db, { workspaceId: opts.workspaceId, retry: true, limit: limit - neverAttempted.length, nowSeconds })
526
538
  : [];
527
- const rows = [...neverAttempted, ...dueRetries];
539
+ const ids = [...neverAttempted, ...dueRetries];
540
+ const rows = [];
541
+ for (let offset = 0; offset < ids.length; offset += 50) {
542
+ rows.push(...await fetchRows(ids.slice(offset, offset + 50)));
543
+ }
544
+ // Attachment hydration must respect the same bind budget as delivery rows.
545
+ const fetchRedriveAttachments = async (workspaceId, messageIds) => {
546
+ const attachments = new Map();
547
+ const uniqueIds = [...new Set(messageIds)];
548
+ for (let offset = 0; offset < uniqueIds.length; offset += 50) {
549
+ const page = await fetchAttachmentsBatch(db, workspaceId, uniqueIds.slice(offset, offset + 50));
550
+ for (const [id, items] of page)
551
+ attachments.set(id, items);
552
+ }
553
+ return attachments;
554
+ };
528
555
  if (!opts.workspaceId && rows.length > 0) {
529
556
  const workspaceAttachments = new Map();
530
557
  for (const workspaceId of [...new Set(rows.map((row) => row.delivery.workspaceId))]) {
531
558
  const workspaceMessageIds = rows
532
559
  .filter((row) => row.delivery.workspaceId === workspaceId)
533
560
  .map((row) => row.delivery.messageId);
534
- const batch = await fetchAttachmentsBatch(db, workspaceId, [...new Set(workspaceMessageIds)]);
561
+ const batch = await fetchRedriveAttachments(workspaceId, workspaceMessageIds);
535
562
  for (const [messageId, attachments] of batch)
536
563
  workspaceAttachments.set(messageId, attachments);
537
564
  }
@@ -547,7 +574,7 @@ export async function fetchDueNodeDeliveryEvents(db, opts = {}) {
547
574
  });
548
575
  }
549
576
  const attachmentsByMessageId = opts.workspaceId
550
- ? await fetchAttachmentsBatch(db, opts.workspaceId, [...new Set(rows.map((row) => row.delivery.messageId))])
577
+ ? await fetchRedriveAttachments(opts.workspaceId, rows.map((row) => row.delivery.messageId))
551
578
  : new Map();
552
579
  return rows.map((row) => {
553
580
  const pendingRow = row;
@@ -623,78 +650,109 @@ export async function fetchQueuedWsBacklogEvents(db, workspaceId, agentId, opts
623
650
  };
624
651
  });
625
652
  }
626
- export async function deliverPendingToNode(db, registry, workspaceId, nodeId, scope = {}) {
627
- const agentIds = scope.agentIds ? [...new Set(scope.agentIds)] : undefined;
628
- if (agentIds?.length === 0)
629
- return 0;
630
- const nowSeconds = Math.floor(Date.now() / 1000);
631
- const conditions = [
632
- eq(deliveries.workspaceId, workspaceId),
633
- eq(agents.locationType, 'via_node'),
634
- eq(agents.locationNodeId, nodeId),
635
- inArray(deliveries.status, [...ACTIVE_DELIVERY_STATUSES]),
636
- gt(deliveries.seq, agents.deliveryAckSeq),
637
- sql `(${deliveries.expiresAt} IS NULL OR ${deliveries.expiresAt} > ${nowSeconds})`,
638
- ];
639
- if (scope.providerName !== undefined) {
640
- conditions.push(eq(agents.providerName, scope.providerName));
641
- }
642
- if (agentIds) {
643
- conditions.push(inArray(agents.id, agentIds));
653
+ const replayFlights = new WeakMap();
654
+ /** Coalesce identical triggers at the socket owner, including a trailing replay
655
+ * when readiness/cursors changed during an outstanding drain. This is local
656
+ * replay coordination, not a global database admission semaphore. */
657
+ export function deliverPendingToNode(db, registry, workspaceId, nodeId, scope = {}) {
658
+ let flights = replayFlights.get(registry);
659
+ if (!flights) {
660
+ flights = new Map();
661
+ replayFlights.set(registry, flights);
644
662
  }
645
- const rows = await db
646
- .select({
647
- delivery: deliveries,
648
- recipientAgentName: agents.name,
649
- recipientProviderName: agents.providerName,
663
+ const key = JSON.stringify([workspaceId, nodeId, scope.providerName,
664
+ scope.agentIds ? [...new Set(scope.agentIds)].sort() : null]);
665
+ return trailingSingleFlight(flights, key, () => replayPendingToNode(db, registry, workspaceId, nodeId, scope));
666
+ }
667
+ /** Drain a finite per-agent high-water mark in ordered, readiness-checked pages. */
668
+ async function replayPendingToNode(db, registry, workspaceId, nodeId, scope = {}) {
669
+ const wantedIds = scope.agentIds ? new Set(scope.agentIds) : undefined;
670
+ if (wantedIds?.size === 0)
671
+ return 0;
672
+ // Resolve the small node roster first. A delivery-first join lets SQLite
673
+ // choose the workspace/status index and scan millions of retained rows.
674
+ const recipients = await db.select({
675
+ id: agents.id, name: agents.name, providerName: agents.providerName,
650
676
  ackSeq: agents.deliveryAckSeq,
651
- body: messages.body,
652
- blocks: messages.blocks,
653
- metadata: messages.metadata,
654
- hasAttachments: messages.hasAttachments,
655
- threadId: messages.threadId,
656
- createdAt: messages.createdAt,
657
- channelId: messages.channelId,
658
- channelName: channels.name,
659
- conversationId: dmConversations.id,
660
- dmType: dmConversations.dmType,
661
- senderAgentId: messages.agentId,
662
- senderAgentName: sql `(
663
- SELECT a.name FROM agents a WHERE a.id = ${messages.agentId}
664
- )`,
665
- })
666
- .from(deliveries)
667
- .innerJoin(agents, eq(deliveries.agentId, agents.id))
668
- .innerJoin(messages, eq(deliveries.messageId, messages.id))
669
- .innerJoin(channels, eq(messages.channelId, channels.id))
670
- .leftJoin(dmConversations, eq(dmConversations.channelId, messages.channelId))
671
- .where(and(...conditions))
672
- .orderBy(asc(agents.name), asc(deliveries.seq));
673
- const attachmentsByMessageId = await fetchAttachmentsBatch(db, workspaceId, [...new Set(rows.map((row) => row.delivery.messageId))]);
674
- const deliveredIds = [];
675
- for (const row of rows) {
676
- if (!isProviderAgentDeliveryReady(registry, workspaceId, nodeId, row.recipientProviderName, row.delivery.agentId)) {
677
+ }).from(agents).where(and(eq(agents.workspaceId, workspaceId), eq(agents.locationType, 'via_node'), eq(agents.locationNodeId, nodeId), scope.providerName === undefined ? undefined : eq(agents.providerName, scope.providerName))).orderBy(asc(agents.name));
678
+ let delivered = 0;
679
+ for (const recipient of recipients) {
680
+ if (wantedIds && !wantedIds.has(recipient.id))
681
+ continue;
682
+ const ready = () => isProviderAgentDeliveryReady(registry, workspaceId, nodeId, recipient.providerName, recipient.id);
683
+ if (!ready())
677
684
  continue;
685
+ // Capture a finite high-water mark: arrivals during replay are handled by
686
+ // live dispatch/the next reconnect, not an indefinitely growing drain.
687
+ const [highWater] = await db.select({ seq: deliveries.seq })
688
+ .from(deliveries).where(and(eq(deliveries.workspaceId, workspaceId), eq(deliveries.agentId, recipient.id))).orderBy(sql `${deliveries.seq} DESC`).limit(1);
689
+ if (!highWater)
690
+ continue;
691
+ let cursor = recipient.ackSeq;
692
+ while (cursor < highWater.seq && ready()) {
693
+ // The literal predicate is essential: SQLite must prove that this
694
+ // query qualifies for the partial index even with bound parameters.
695
+ const page = await db.select({ id: sql `${deliveries.id}`, seq: sql `${deliveries.seq}` })
696
+ .from(sql `${deliveries} INDEXED BY idx_deliveries_agent_active_seq`)
697
+ .where(and(eq(deliveries.workspaceId, workspaceId), eq(deliveries.agentId, recipient.id), sql `${deliveries.status} IN ('queued', 'delivered')`, gt(deliveries.seq, cursor), lte(deliveries.seq, highWater.seq))).orderBy(asc(deliveries.seq)).limit(50);
698
+ if (!page.length)
699
+ break;
700
+ cursor = page[page.length - 1].seq;
701
+ // Hydrate only this bounded ID page. Re-check ownership, ACK, expiry and
702
+ // status here because each await can race a handoff or cumulative ACK.
703
+ const rows = await db.select({
704
+ delivery: indexedDeliveryColumns,
705
+ recipientAgentName: agents.name,
706
+ recipientProviderName: agents.providerName,
707
+ ackSeq: agents.deliveryAckSeq,
708
+ body: messages.body, blocks: messages.blocks, metadata: messages.metadata,
709
+ hasAttachments: messages.hasAttachments, threadId: messages.threadId,
710
+ createdAt: messages.createdAt, channelId: messages.channelId,
711
+ channelName: channels.name, conversationId: dmConversations.id,
712
+ dmType: dmConversations.dmType, senderAgentId: messages.agentId,
713
+ senderAgentName: sql `(
714
+ SELECT a.name FROM agents a WHERE a.id = ${messages.agentId}
715
+ )`,
716
+ }).from(sql `${deliveries} INDEXED BY idx_deliveries_id_lookup`)
717
+ .innerJoin(agents, eq(deliveries.agentId, agents.id))
718
+ .innerJoin(messages, eq(deliveries.messageId, messages.id))
719
+ .innerJoin(channels, eq(messages.channelId, channels.id))
720
+ .leftJoin(dmConversations, eq(dmConversations.channelId, messages.channelId))
721
+ .where(and(inArray(deliveries.id, page.map(row => row.id)), eq(deliveries.workspaceId, workspaceId), eq(agents.locationType, 'via_node'), eq(agents.locationNodeId, nodeId), eq(agents.id, recipient.id), recipient.providerName === null
722
+ ? isNull(agents.providerName) : eq(agents.providerName, recipient.providerName), inArray(deliveries.status, [...ACTIVE_DELIVERY_STATUSES]), gt(deliveries.seq, agents.deliveryAckSeq), sql `(${deliveries.expiresAt} IS NULL OR ${deliveries.expiresAt} > ${Math.floor(Date.now() / 1000)})`)).orderBy(asc(deliveries.seq));
723
+ const attachments = await fetchAttachmentsBatch(db, workspaceId, [...new Set(rows.map(row => row.delivery.messageId))]);
724
+ const deliveredIds = [];
725
+ let interrupted = false;
726
+ for (const row of rows) {
727
+ if (!ready()) {
728
+ interrupted = true;
729
+ break;
730
+ }
731
+ const { eventType, eventData } = buildRoutableDeliveryEvent(row, attachments.get(row.delivery.messageId) ?? []);
732
+ const sent = await registry.sendToProvider(workspaceId, nodeId, recipient.providerName, buildDeliverFrame({
733
+ delivery_id: row.delivery.id, agent_id: recipient.id,
734
+ agent: row.recipientAgentName, msg_id: row.delivery.messageId,
735
+ seq: row.delivery.seq, mode: wireMode(row.delivery.mode),
736
+ payload: buildDeliverPayload(eventType, eventData),
737
+ }));
738
+ // Never send a higher sequence past a failed lower one: a cumulative
739
+ // ACK for it would make the unsent lower delivery replay-invisible.
740
+ if (!sent) {
741
+ interrupted = true;
742
+ break;
743
+ }
744
+ deliveredIds.push(row.delivery.id);
745
+ }
746
+ await markDeliveriesDelivered(db, workspaceId, deliveredIds);
747
+ delivered += deliveredIds.length;
748
+ if (interrupted)
749
+ break;
678
750
  }
679
- const attachments = attachmentsByMessageId.get(row.delivery.messageId) ?? [];
680
- const { eventType, eventData } = buildRoutableDeliveryEvent(row, attachments);
681
- const sent = await registry.sendToProvider(workspaceId, nodeId, row.recipientProviderName, buildDeliverFrame({
682
- delivery_id: row.delivery.id,
683
- agent_id: row.delivery.agentId,
684
- agent: row.recipientAgentName,
685
- msg_id: row.delivery.messageId,
686
- seq: row.delivery.seq,
687
- mode: wireMode(row.delivery.mode),
688
- payload: buildDeliverPayload(eventType, eventData),
689
- }));
690
- if (sent)
691
- deliveredIds.push(row.delivery.id);
692
751
  }
693
- await markDeliveriesDelivered(db, workspaceId, deliveredIds);
694
- return deliveredIds.length;
752
+ return delivered;
695
753
  }
696
754
  /**
697
- * Resolve the result of a status-guarded transition: when the write landed,
755
+ * Resolve a status-guarded transition: when the write landed,
698
756
  * report the updated row as changed. When it did not (the row was deleted, or a
699
757
  * concurrent ack won the race and the row is now terminal), re-read and return
700
758
  * the current state as unchanged — never resurrecting it or emitting an event.