@relaycast/engine 8.5.2 → 8.5.4
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/adapters/node/database.d.ts.map +1 -1
- package/dist/adapters/node/database.js +4 -3
- package/dist/adapters/node/database.js.map +1 -1
- package/dist/db/migrationPlan.d.ts +3 -0
- package/dist/db/migrationPlan.d.ts.map +1 -0
- package/dist/db/migrationPlan.js +19 -0
- package/dist/db/migrationPlan.js.map +1 -0
- package/dist/db/migrations/0048_bounded_maintenance.sql +20 -0
- package/dist/db/migrations/0049_redrive_review_hardening.sql +11 -0
- package/dist/db/migrations/0050_compact_maintenance_indexes.sql +42 -0
- package/dist/db/migrations/supersessions.json +4 -0
- package/dist/db/schema.d.ts +46 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +24 -2
- package/dist/db/schema.js.map +1 -1
- package/dist/engine/action.d.ts.map +1 -1
- package/dist/engine/action.js +2 -1
- package/dist/engine/action.js.map +1 -1
- package/dist/engine/boundedRetention.d.ts +13 -0
- package/dist/engine/boundedRetention.d.ts.map +1 -0
- package/dist/engine/boundedRetention.js +204 -0
- package/dist/engine/boundedRetention.js.map +1 -0
- package/dist/engine/delivery.d.ts +5 -0
- package/dist/engine/delivery.d.ts.map +1 -1
- package/dist/engine/delivery.js +154 -96
- package/dist/engine/delivery.js.map +1 -1
- package/dist/engine/deliveryWrites.d.ts.map +1 -1
- package/dist/engine/deliveryWrites.js +15 -6
- package/dist/engine/deliveryWrites.js.map +1 -1
- package/dist/engine/invocationRetry.d.ts +3 -0
- package/dist/engine/invocationRetry.d.ts.map +1 -0
- package/dist/engine/invocationRetry.js +5 -0
- package/dist/engine/invocationRetry.js.map +1 -0
- package/dist/engine/nodeRedriveCandidates.d.ts +15 -0
- package/dist/engine/nodeRedriveCandidates.d.ts.map +1 -0
- package/dist/engine/nodeRedriveCandidates.js +72 -0
- package/dist/engine/nodeRedriveCandidates.js.map +1 -0
- package/dist/engine/retention.d.ts +5 -36
- package/dist/engine/retention.d.ts.map +1 -1
- package/dist/engine/retention.js +6 -197
- package/dist/engine/retention.js.map +1 -1
- package/dist/lib/settlePool.d.ts +3 -0
- package/dist/lib/settlePool.d.ts.map +1 -0
- package/dist/lib/settlePool.js +18 -0
- package/dist/lib/settlePool.js.map +1 -0
- package/dist/lib/singleFlight.d.ts +6 -0
- package/dist/lib/singleFlight.d.ts.map +1 -0
- package/dist/lib/singleFlight.js +36 -0
- package/dist/lib/singleFlight.js.map +1 -0
- package/dist/routes/deliveryRouting.d.ts.map +1 -1
- package/dist/routes/deliveryRouting.js +8 -4
- package/dist/routes/deliveryRouting.js.map +1 -1
- 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":"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;
|
|
1
|
+
{"version":3,"file":"delivery.d.ts","sourceRoot":"","sources":["../../src/engine/delivery.ts"],"names":[],"mappings":"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"}
|
package/dist/engine/delivery.js
CHANGED
|
@@ -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
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
.
|
|
293
|
-
|
|
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
|
|
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 = (
|
|
506
|
+
const fetchRows = (ids) => db
|
|
493
507
|
.select({
|
|
494
|
-
delivery:
|
|
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,
|
|
517
|
-
.orderBy(
|
|
518
|
-
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
|
|
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
|
|
537
|
+
? await readNodeRedriveCandidates(db, { workspaceId: opts.workspaceId, retry: true, limit: limit - neverAttempted.length, nowSeconds })
|
|
526
538
|
: [];
|
|
527
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
|
646
|
-
.
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
694
|
-
return deliveredIds.length;
|
|
752
|
+
return delivered;
|
|
695
753
|
}
|
|
696
754
|
/**
|
|
697
|
-
* Resolve
|
|
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.
|