@diegoaltoworks/chatter 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/channels/whatsapp/images.d.ts +85 -0
  2. package/dist/channels/whatsapp/images.d.ts.map +1 -0
  3. package/dist/channels/whatsapp/images.test.d.ts +2 -0
  4. package/dist/channels/whatsapp/images.test.d.ts.map +1 -0
  5. package/dist/channels/whatsapp/inbound.d.ts +9 -0
  6. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  7. package/dist/channels/whatsapp/index.d.ts +1 -0
  8. package/dist/channels/whatsapp/index.d.ts.map +1 -1
  9. package/dist/channels/whatsapp/index.js +82 -0
  10. package/dist/channels/whatsapp/index.mjs +79 -0
  11. package/dist/scheduler/claimStore.d.ts +27 -0
  12. package/dist/scheduler/claimStore.d.ts.map +1 -0
  13. package/dist/scheduler/claimStore.test.d.ts +2 -0
  14. package/dist/scheduler/claimStore.test.d.ts.map +1 -0
  15. package/dist/scheduler/compose.d.ts +23 -0
  16. package/dist/scheduler/compose.d.ts.map +1 -0
  17. package/dist/scheduler/compose.test.d.ts +2 -0
  18. package/dist/scheduler/compose.test.d.ts.map +1 -0
  19. package/dist/scheduler/deliver.d.ts +10 -0
  20. package/dist/scheduler/deliver.d.ts.map +1 -0
  21. package/dist/scheduler/deliver.test.d.ts +2 -0
  22. package/dist/scheduler/deliver.test.d.ts.map +1 -0
  23. package/dist/scheduler/dueDrops.d.ts +13 -0
  24. package/dist/scheduler/dueDrops.d.ts.map +1 -0
  25. package/dist/scheduler/dueDrops.test.d.ts +2 -0
  26. package/dist/scheduler/dueDrops.test.d.ts.map +1 -0
  27. package/dist/scheduler/index.d.ts +31 -0
  28. package/dist/scheduler/index.d.ts.map +1 -0
  29. package/dist/scheduler/index.js +332 -0
  30. package/dist/scheduler/index.mjs +290 -0
  31. package/dist/scheduler/scheduler.d.ts +44 -0
  32. package/dist/scheduler/scheduler.d.ts.map +1 -0
  33. package/dist/scheduler/scheduler.test.d.ts +2 -0
  34. package/dist/scheduler/scheduler.test.d.ts.map +1 -0
  35. package/dist/scheduler/tick.d.ts +19 -0
  36. package/dist/scheduler/tick.d.ts.map +1 -0
  37. package/dist/scheduler/tick.test.d.ts +2 -0
  38. package/dist/scheduler/tick.test.d.ts.map +1 -0
  39. package/dist/scheduler/types.d.ts +36 -0
  40. package/dist/scheduler/types.d.ts.map +1 -0
  41. package/package.json +8 -2
@@ -0,0 +1,332 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/scheduler/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ DEFAULT_CLAIM_TABLE: () => DEFAULT_CLAIM_TABLE,
24
+ DEFAULT_CLAIM_TIMEOUT_MS: () => DEFAULT_CLAIM_TIMEOUT_MS,
25
+ DEFAULT_FALLBACK_MESSAGE: () => DEFAULT_FALLBACK_MESSAGE,
26
+ DEFAULT_GRACE_MS: () => DEFAULT_GRACE_MS,
27
+ DEFAULT_INTERVAL_MS: () => DEFAULT_INTERVAL_MS,
28
+ canClaim: () => canClaim,
29
+ composeMessage: () => composeMessage,
30
+ createAnswerCompose: () => createAnswerCompose,
31
+ createScheduler: () => createScheduler,
32
+ createTursoScheduleClaimStore: () => createTursoScheduleClaimStore,
33
+ deliver: () => deliver,
34
+ dueDrops: () => dueDrops,
35
+ isDue: () => isDue,
36
+ isWithinGrace: () => isWithinGrace,
37
+ runTick: () => runTick,
38
+ staleDrops: () => staleDrops
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/scheduler/claimStore.ts
43
+ var DEFAULT_CLAIM_TABLE = "chatter_schedule_claims";
44
+ var DEFAULT_CLAIM_TIMEOUT_MS = 2 * 6e4;
45
+ var VALID_TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
46
+ function canClaim(existing, instanceId, now, claimTimeoutMs) {
47
+ if (!existing) return true;
48
+ if (existing.instanceId === instanceId) return true;
49
+ return now - existing.claimedAt >= claimTimeoutMs;
50
+ }
51
+ var schemaReady = /* @__PURE__ */ new WeakMap();
52
+ function ensureSchema(client, tableName) {
53
+ let perTable = schemaReady.get(client);
54
+ if (!perTable) {
55
+ perTable = /* @__PURE__ */ new Map();
56
+ schemaReady.set(client, perTable);
57
+ }
58
+ let ready = perTable.get(tableName);
59
+ if (!ready) {
60
+ const table = perTable;
61
+ ready = client.execute(
62
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
63
+ schedule_id TEXT PRIMARY KEY,
64
+ instance_id TEXT NOT NULL,
65
+ claimed_at INTEGER NOT NULL
66
+ )`
67
+ ).catch((error) => {
68
+ table.delete(tableName);
69
+ throw error;
70
+ });
71
+ perTable.set(tableName, ready);
72
+ }
73
+ return ready;
74
+ }
75
+ function createTursoScheduleClaimStore(client, instanceId, tableName = DEFAULT_CLAIM_TABLE, claimTimeoutMs = DEFAULT_CLAIM_TIMEOUT_MS) {
76
+ if (!VALID_TABLE_NAME.test(tableName)) {
77
+ throw new Error(`Invalid schedule claim table name: ${tableName}`);
78
+ }
79
+ return {
80
+ async claim(id, now) {
81
+ await ensureSchema(client, tableName);
82
+ const result = await client.execute({
83
+ sql: `INSERT INTO ${tableName} (schedule_id, instance_id, claimed_at) VALUES (?, ?, ?)
84
+ ON CONFLICT(schedule_id) DO UPDATE SET
85
+ instance_id = excluded.instance_id,
86
+ claimed_at = excluded.claimed_at
87
+ WHERE ${tableName}.instance_id = excluded.instance_id
88
+ OR ${tableName}.claimed_at <= excluded.claimed_at - ?
89
+ RETURNING instance_id`,
90
+ args: [id, instanceId, now, claimTimeoutMs]
91
+ });
92
+ const row = result.rows[0];
93
+ return Boolean(row) && row.instance_id === instanceId;
94
+ },
95
+ async release(id) {
96
+ await ensureSchema(client, tableName);
97
+ await client.execute({
98
+ sql: `DELETE FROM ${tableName} WHERE schedule_id = ? AND instance_id = ?`,
99
+ args: [id, instanceId]
100
+ });
101
+ }
102
+ };
103
+ }
104
+
105
+ // src/core/guardrails.ts
106
+ function detectLeakage(text) {
107
+ const flags = [
108
+ /system prompt/i,
109
+ /hidden instruction/i,
110
+ /here (are|is) (my|the) rules/i,
111
+ /BEGIN SYSTEM PROMPT/i,
112
+ /tool instructions/i
113
+ ];
114
+ return flags.some((rx) => rx.test(text));
115
+ }
116
+ function scrubOutput(text) {
117
+ return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
118
+ }
119
+ function guardOutput(text) {
120
+ if (detectLeakage(text)) {
121
+ return "Sorry, I can't share internal instructions. How else can I help?";
122
+ }
123
+ return scrubOutput(text);
124
+ }
125
+
126
+ // src/core/llm.ts
127
+ var DEFAULT_MODEL = "gpt-4o";
128
+ async function completeOnce({
129
+ client,
130
+ system,
131
+ messages,
132
+ temperature = 0.2,
133
+ model = DEFAULT_MODEL
134
+ }) {
135
+ const res = await client.chat.completions.create({
136
+ model,
137
+ temperature,
138
+ messages: [{ role: "system", content: system }, ...messages]
139
+ });
140
+ const text = res.choices[0]?.message?.content ?? "";
141
+ return {
142
+ content: guardOutput(text),
143
+ usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
144
+ };
145
+ }
146
+
147
+ // src/core/answer.ts
148
+ var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
149
+ async function runAnswerFn(answerFn, { system, messages, mode, sender }) {
150
+ const result = await answerFn({ system, messages, mode, sender });
151
+ const raw = typeof result === "string" ? result : result?.content;
152
+ const usage = typeof result === "string" ? void 0 : result?.usage;
153
+ return {
154
+ content: guardOutput(typeof raw === "string" ? raw : ""),
155
+ usage: usage ?? noUsage()
156
+ };
157
+ }
158
+ async function answerOnce({
159
+ answerFn,
160
+ client,
161
+ system,
162
+ messages,
163
+ mode,
164
+ sender,
165
+ temperature,
166
+ model
167
+ }) {
168
+ if (answerFn) {
169
+ return runAnswerFn(answerFn, { system, messages, mode, sender });
170
+ }
171
+ return completeOnce({ client, system, messages, temperature, model });
172
+ }
173
+
174
+ // src/scheduler/compose.ts
175
+ async function composeMessage(entry, compose, fallbackMessage) {
176
+ if (!compose) return fallbackMessage;
177
+ try {
178
+ const result = await compose(entry);
179
+ return result.trim().length > 0 ? result : fallbackMessage;
180
+ } catch (error) {
181
+ console.warn(`Scheduler compose failed for entry "${entry.id}":`, error);
182
+ return fallbackMessage;
183
+ }
184
+ }
185
+ function createAnswerCompose(options) {
186
+ return async (entry) => {
187
+ const { content } = await answerOnce({
188
+ client: options.client,
189
+ system: options.system,
190
+ messages: options.buildMessages(entry),
191
+ mode: options.mode ?? "private",
192
+ answerFn: options.answerFn,
193
+ temperature: options.temperature,
194
+ model: options.model
195
+ });
196
+ return content;
197
+ };
198
+ }
199
+
200
+ // src/scheduler/deliver.ts
201
+ async function deliver(entry, message, options) {
202
+ if (options.voiceAttempted) {
203
+ const voiceSent = await options.senders.sendVoice(entry.channel, entry.chatId, message);
204
+ if (voiceSent) return true;
205
+ }
206
+ return options.senders.sendText(entry.channel, entry.chatId, message);
207
+ }
208
+
209
+ // src/scheduler/dueDrops.ts
210
+ function isDue(entry, now) {
211
+ return entry.fireAt <= now;
212
+ }
213
+ function isWithinGrace(entry, now, graceMs) {
214
+ return now - entry.fireAt <= graceMs;
215
+ }
216
+ function dueDrops(entries, now, graceMs) {
217
+ return entries.filter((entry) => isDue(entry, now) && isWithinGrace(entry, now, graceMs));
218
+ }
219
+ function staleDrops(entries, now, graceMs) {
220
+ return entries.filter((entry) => isDue(entry, now) && !isWithinGrace(entry, now, graceMs));
221
+ }
222
+
223
+ // src/scheduler/tick.ts
224
+ async function runTick(entries, options) {
225
+ const result = { sent: [], notClaimed: [], stale: [], failed: [] };
226
+ for (const entry of staleDrops(entries, options.now, options.graceMs)) {
227
+ result.stale.push(entry.id);
228
+ }
229
+ for (const entry of dueDrops(entries, options.now, options.graceMs)) {
230
+ const claimed = await options.claims.claim(entry.id, options.now);
231
+ if (!claimed) {
232
+ result.notClaimed.push(entry.id);
233
+ continue;
234
+ }
235
+ try {
236
+ const message = await composeMessage(entry, options.compose, options.fallbackMessage);
237
+ const delivered = await deliver(entry, message, {
238
+ senders: options.senders,
239
+ voiceAttempted: options.voiceAttempted
240
+ });
241
+ if (delivered) {
242
+ result.sent.push(entry.id);
243
+ } else {
244
+ await options.claims.release(entry.id);
245
+ result.failed.push(entry.id);
246
+ }
247
+ } catch (error) {
248
+ console.warn(`Scheduler delivery failed for entry "${entry.id}":`, error);
249
+ await options.claims.release(entry.id);
250
+ result.failed.push(entry.id);
251
+ }
252
+ }
253
+ return result;
254
+ }
255
+
256
+ // src/scheduler/scheduler.ts
257
+ var DEFAULT_GRACE_MS = 5 * 6e4;
258
+ var DEFAULT_INTERVAL_MS = 6e4;
259
+ var DEFAULT_FALLBACK_MESSAGE = "You have a scheduled message.";
260
+ var defaultSchedule = (fn, ms) => {
261
+ const timer = setInterval(fn, ms);
262
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
263
+ return { stop: () => clearInterval(timer) };
264
+ };
265
+ function createScheduler(config) {
266
+ const claims = createTursoScheduleClaimStore(
267
+ config.db,
268
+ config.instanceId ?? crypto.randomUUID(),
269
+ config.tableName ?? DEFAULT_CLAIM_TABLE,
270
+ config.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS
271
+ );
272
+ const graceMs = config.graceMs ?? DEFAULT_GRACE_MS;
273
+ const fallbackMessage = config.fallbackMessage ?? DEFAULT_FALLBACK_MESSAGE;
274
+ const now = config.now ?? Date.now;
275
+ const schedule = config.schedule ?? defaultSchedule;
276
+ let running;
277
+ let ticking = false;
278
+ async function tickOnce() {
279
+ const entries = await config.fetchPending();
280
+ const result = await runTick(entries, {
281
+ claims,
282
+ senders: config.senders,
283
+ now: now(),
284
+ graceMs,
285
+ fallbackMessage,
286
+ compose: config.compose,
287
+ voiceAttempted: config.voiceAttempted
288
+ });
289
+ config.onTick?.(result);
290
+ return result;
291
+ }
292
+ return {
293
+ start() {
294
+ if (running) return;
295
+ running = schedule(() => {
296
+ if (ticking) return;
297
+ ticking = true;
298
+ tickOnce().catch((error) => {
299
+ if (config.onError) config.onError(error);
300
+ else console.warn("Scheduler tick failed:", error);
301
+ }).finally(() => {
302
+ ticking = false;
303
+ });
304
+ }, config.intervalMs ?? DEFAULT_INTERVAL_MS);
305
+ },
306
+ stop() {
307
+ if (!running) return;
308
+ running.stop();
309
+ running = void 0;
310
+ },
311
+ tickOnce
312
+ };
313
+ }
314
+ // Annotate the CommonJS export names for ESM import in node:
315
+ 0 && (module.exports = {
316
+ DEFAULT_CLAIM_TABLE,
317
+ DEFAULT_CLAIM_TIMEOUT_MS,
318
+ DEFAULT_FALLBACK_MESSAGE,
319
+ DEFAULT_GRACE_MS,
320
+ DEFAULT_INTERVAL_MS,
321
+ canClaim,
322
+ composeMessage,
323
+ createAnswerCompose,
324
+ createScheduler,
325
+ createTursoScheduleClaimStore,
326
+ deliver,
327
+ dueDrops,
328
+ isDue,
329
+ isWithinGrace,
330
+ runTick,
331
+ staleDrops
332
+ });
@@ -0,0 +1,290 @@
1
+ // src/scheduler/claimStore.ts
2
+ var DEFAULT_CLAIM_TABLE = "chatter_schedule_claims";
3
+ var DEFAULT_CLAIM_TIMEOUT_MS = 2 * 6e4;
4
+ var VALID_TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
5
+ function canClaim(existing, instanceId, now, claimTimeoutMs) {
6
+ if (!existing) return true;
7
+ if (existing.instanceId === instanceId) return true;
8
+ return now - existing.claimedAt >= claimTimeoutMs;
9
+ }
10
+ var schemaReady = /* @__PURE__ */ new WeakMap();
11
+ function ensureSchema(client, tableName) {
12
+ let perTable = schemaReady.get(client);
13
+ if (!perTable) {
14
+ perTable = /* @__PURE__ */ new Map();
15
+ schemaReady.set(client, perTable);
16
+ }
17
+ let ready = perTable.get(tableName);
18
+ if (!ready) {
19
+ const table = perTable;
20
+ ready = client.execute(
21
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
22
+ schedule_id TEXT PRIMARY KEY,
23
+ instance_id TEXT NOT NULL,
24
+ claimed_at INTEGER NOT NULL
25
+ )`
26
+ ).catch((error) => {
27
+ table.delete(tableName);
28
+ throw error;
29
+ });
30
+ perTable.set(tableName, ready);
31
+ }
32
+ return ready;
33
+ }
34
+ function createTursoScheduleClaimStore(client, instanceId, tableName = DEFAULT_CLAIM_TABLE, claimTimeoutMs = DEFAULT_CLAIM_TIMEOUT_MS) {
35
+ if (!VALID_TABLE_NAME.test(tableName)) {
36
+ throw new Error(`Invalid schedule claim table name: ${tableName}`);
37
+ }
38
+ return {
39
+ async claim(id, now) {
40
+ await ensureSchema(client, tableName);
41
+ const result = await client.execute({
42
+ sql: `INSERT INTO ${tableName} (schedule_id, instance_id, claimed_at) VALUES (?, ?, ?)
43
+ ON CONFLICT(schedule_id) DO UPDATE SET
44
+ instance_id = excluded.instance_id,
45
+ claimed_at = excluded.claimed_at
46
+ WHERE ${tableName}.instance_id = excluded.instance_id
47
+ OR ${tableName}.claimed_at <= excluded.claimed_at - ?
48
+ RETURNING instance_id`,
49
+ args: [id, instanceId, now, claimTimeoutMs]
50
+ });
51
+ const row = result.rows[0];
52
+ return Boolean(row) && row.instance_id === instanceId;
53
+ },
54
+ async release(id) {
55
+ await ensureSchema(client, tableName);
56
+ await client.execute({
57
+ sql: `DELETE FROM ${tableName} WHERE schedule_id = ? AND instance_id = ?`,
58
+ args: [id, instanceId]
59
+ });
60
+ }
61
+ };
62
+ }
63
+
64
+ // src/core/guardrails.ts
65
+ function detectLeakage(text) {
66
+ const flags = [
67
+ /system prompt/i,
68
+ /hidden instruction/i,
69
+ /here (are|is) (my|the) rules/i,
70
+ /BEGIN SYSTEM PROMPT/i,
71
+ /tool instructions/i
72
+ ];
73
+ return flags.some((rx) => rx.test(text));
74
+ }
75
+ function scrubOutput(text) {
76
+ return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
77
+ }
78
+ function guardOutput(text) {
79
+ if (detectLeakage(text)) {
80
+ return "Sorry, I can't share internal instructions. How else can I help?";
81
+ }
82
+ return scrubOutput(text);
83
+ }
84
+
85
+ // src/core/llm.ts
86
+ var DEFAULT_MODEL = "gpt-4o";
87
+ async function completeOnce({
88
+ client,
89
+ system,
90
+ messages,
91
+ temperature = 0.2,
92
+ model = DEFAULT_MODEL
93
+ }) {
94
+ const res = await client.chat.completions.create({
95
+ model,
96
+ temperature,
97
+ messages: [{ role: "system", content: system }, ...messages]
98
+ });
99
+ const text = res.choices[0]?.message?.content ?? "";
100
+ return {
101
+ content: guardOutput(text),
102
+ usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
103
+ };
104
+ }
105
+
106
+ // src/core/answer.ts
107
+ var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
108
+ async function runAnswerFn(answerFn, { system, messages, mode, sender }) {
109
+ const result = await answerFn({ system, messages, mode, sender });
110
+ const raw = typeof result === "string" ? result : result?.content;
111
+ const usage = typeof result === "string" ? void 0 : result?.usage;
112
+ return {
113
+ content: guardOutput(typeof raw === "string" ? raw : ""),
114
+ usage: usage ?? noUsage()
115
+ };
116
+ }
117
+ async function answerOnce({
118
+ answerFn,
119
+ client,
120
+ system,
121
+ messages,
122
+ mode,
123
+ sender,
124
+ temperature,
125
+ model
126
+ }) {
127
+ if (answerFn) {
128
+ return runAnswerFn(answerFn, { system, messages, mode, sender });
129
+ }
130
+ return completeOnce({ client, system, messages, temperature, model });
131
+ }
132
+
133
+ // src/scheduler/compose.ts
134
+ async function composeMessage(entry, compose, fallbackMessage) {
135
+ if (!compose) return fallbackMessage;
136
+ try {
137
+ const result = await compose(entry);
138
+ return result.trim().length > 0 ? result : fallbackMessage;
139
+ } catch (error) {
140
+ console.warn(`Scheduler compose failed for entry "${entry.id}":`, error);
141
+ return fallbackMessage;
142
+ }
143
+ }
144
+ function createAnswerCompose(options) {
145
+ return async (entry) => {
146
+ const { content } = await answerOnce({
147
+ client: options.client,
148
+ system: options.system,
149
+ messages: options.buildMessages(entry),
150
+ mode: options.mode ?? "private",
151
+ answerFn: options.answerFn,
152
+ temperature: options.temperature,
153
+ model: options.model
154
+ });
155
+ return content;
156
+ };
157
+ }
158
+
159
+ // src/scheduler/deliver.ts
160
+ async function deliver(entry, message, options) {
161
+ if (options.voiceAttempted) {
162
+ const voiceSent = await options.senders.sendVoice(entry.channel, entry.chatId, message);
163
+ if (voiceSent) return true;
164
+ }
165
+ return options.senders.sendText(entry.channel, entry.chatId, message);
166
+ }
167
+
168
+ // src/scheduler/dueDrops.ts
169
+ function isDue(entry, now) {
170
+ return entry.fireAt <= now;
171
+ }
172
+ function isWithinGrace(entry, now, graceMs) {
173
+ return now - entry.fireAt <= graceMs;
174
+ }
175
+ function dueDrops(entries, now, graceMs) {
176
+ return entries.filter((entry) => isDue(entry, now) && isWithinGrace(entry, now, graceMs));
177
+ }
178
+ function staleDrops(entries, now, graceMs) {
179
+ return entries.filter((entry) => isDue(entry, now) && !isWithinGrace(entry, now, graceMs));
180
+ }
181
+
182
+ // src/scheduler/tick.ts
183
+ async function runTick(entries, options) {
184
+ const result = { sent: [], notClaimed: [], stale: [], failed: [] };
185
+ for (const entry of staleDrops(entries, options.now, options.graceMs)) {
186
+ result.stale.push(entry.id);
187
+ }
188
+ for (const entry of dueDrops(entries, options.now, options.graceMs)) {
189
+ const claimed = await options.claims.claim(entry.id, options.now);
190
+ if (!claimed) {
191
+ result.notClaimed.push(entry.id);
192
+ continue;
193
+ }
194
+ try {
195
+ const message = await composeMessage(entry, options.compose, options.fallbackMessage);
196
+ const delivered = await deliver(entry, message, {
197
+ senders: options.senders,
198
+ voiceAttempted: options.voiceAttempted
199
+ });
200
+ if (delivered) {
201
+ result.sent.push(entry.id);
202
+ } else {
203
+ await options.claims.release(entry.id);
204
+ result.failed.push(entry.id);
205
+ }
206
+ } catch (error) {
207
+ console.warn(`Scheduler delivery failed for entry "${entry.id}":`, error);
208
+ await options.claims.release(entry.id);
209
+ result.failed.push(entry.id);
210
+ }
211
+ }
212
+ return result;
213
+ }
214
+
215
+ // src/scheduler/scheduler.ts
216
+ var DEFAULT_GRACE_MS = 5 * 6e4;
217
+ var DEFAULT_INTERVAL_MS = 6e4;
218
+ var DEFAULT_FALLBACK_MESSAGE = "You have a scheduled message.";
219
+ var defaultSchedule = (fn, ms) => {
220
+ const timer = setInterval(fn, ms);
221
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
222
+ return { stop: () => clearInterval(timer) };
223
+ };
224
+ function createScheduler(config) {
225
+ const claims = createTursoScheduleClaimStore(
226
+ config.db,
227
+ config.instanceId ?? crypto.randomUUID(),
228
+ config.tableName ?? DEFAULT_CLAIM_TABLE,
229
+ config.claimTimeoutMs ?? DEFAULT_CLAIM_TIMEOUT_MS
230
+ );
231
+ const graceMs = config.graceMs ?? DEFAULT_GRACE_MS;
232
+ const fallbackMessage = config.fallbackMessage ?? DEFAULT_FALLBACK_MESSAGE;
233
+ const now = config.now ?? Date.now;
234
+ const schedule = config.schedule ?? defaultSchedule;
235
+ let running;
236
+ let ticking = false;
237
+ async function tickOnce() {
238
+ const entries = await config.fetchPending();
239
+ const result = await runTick(entries, {
240
+ claims,
241
+ senders: config.senders,
242
+ now: now(),
243
+ graceMs,
244
+ fallbackMessage,
245
+ compose: config.compose,
246
+ voiceAttempted: config.voiceAttempted
247
+ });
248
+ config.onTick?.(result);
249
+ return result;
250
+ }
251
+ return {
252
+ start() {
253
+ if (running) return;
254
+ running = schedule(() => {
255
+ if (ticking) return;
256
+ ticking = true;
257
+ tickOnce().catch((error) => {
258
+ if (config.onError) config.onError(error);
259
+ else console.warn("Scheduler tick failed:", error);
260
+ }).finally(() => {
261
+ ticking = false;
262
+ });
263
+ }, config.intervalMs ?? DEFAULT_INTERVAL_MS);
264
+ },
265
+ stop() {
266
+ if (!running) return;
267
+ running.stop();
268
+ running = void 0;
269
+ },
270
+ tickOnce
271
+ };
272
+ }
273
+ export {
274
+ DEFAULT_CLAIM_TABLE,
275
+ DEFAULT_CLAIM_TIMEOUT_MS,
276
+ DEFAULT_FALLBACK_MESSAGE,
277
+ DEFAULT_GRACE_MS,
278
+ DEFAULT_INTERVAL_MS,
279
+ canClaim,
280
+ composeMessage,
281
+ createAnswerCompose,
282
+ createScheduler,
283
+ createTursoScheduleClaimStore,
284
+ deliver,
285
+ dueDrops,
286
+ isDue,
287
+ isWithinGrace,
288
+ runTick,
289
+ staleDrops
290
+ };
@@ -0,0 +1,44 @@
1
+ import type { Client } from "@libsql/client";
2
+ import type { ChannelSenderRegistry } from "../channels/senders";
3
+ import type { ComposeFn, ScheduleEntry, TickResult } from "./types";
4
+ export declare const DEFAULT_GRACE_MS: number;
5
+ export declare const DEFAULT_INTERVAL_MS = 60000;
6
+ export declare const DEFAULT_FALLBACK_MESSAGE = "You have a scheduled message.";
7
+ export interface SchedulerConfig {
8
+ db: Client;
9
+ senders: ChannelSenderRegistry;
10
+ /** Returns the caller's own pending candidates for this tick — chatter stores no schedule content. */
11
+ fetchPending: () => Promise<ScheduleEntry[]> | ScheduleEntry[];
12
+ tableName?: string;
13
+ graceMs?: number;
14
+ intervalMs?: number;
15
+ claimTimeoutMs?: number;
16
+ instanceId?: string;
17
+ compose?: ComposeFn;
18
+ fallbackMessage?: string;
19
+ voiceAttempted?: boolean;
20
+ onTick?: (result: TickResult) => void;
21
+ onError?: (error: unknown) => void;
22
+ /** Injectable for tests; defaults to `setInterval`/`clearInterval`. */
23
+ schedule?: (fn: () => void, ms: number) => {
24
+ stop: () => void;
25
+ };
26
+ /** Injectable for tests; defaults to `Date.now`. */
27
+ now?: () => number;
28
+ }
29
+ export interface Scheduler {
30
+ /** Starts ticking on `intervalMs`. No-op if already started. */
31
+ start(): void;
32
+ /** Stops ticking. No-op if not started. */
33
+ stop(): void;
34
+ /** Runs one tick immediately, outside the interval — useful for tests and manual triggers. */
35
+ tickOnce(): Promise<TickResult>;
36
+ }
37
+ /**
38
+ * The ticker: on each interval, pulls pending candidates from the caller,
39
+ * claims each due one exactly once across every running instance, composes
40
+ * and delivers it, and releases the claim on a failed send so it retries
41
+ * within the grace window instead of being lost.
42
+ */
43
+ export declare function createScheduler(config: SchedulerConfig): Scheduler;
44
+ //# sourceMappingURL=scheduler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/scheduler/scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAOjE,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAEpE,eAAO,MAAM,gBAAgB,QAAa,CAAC;AAC3C,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAC1C,eAAO,MAAM,wBAAwB,kCAAkC,CAAC;AAExE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,qBAAqB,CAAC;IAC/B,sGAAsG;IACtG,YAAY,EAAE,MAAM,OAAO,CAAC,aAAa,EAAE,CAAC,GAAG,aAAa,EAAE,CAAC;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,uEAAuE;IACvE,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IAChE,oDAAoD;IACpD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,gEAAgE;IAChE,KAAK,IAAI,IAAI,CAAC;IACd,2CAA2C;IAC3C,IAAI,IAAI,IAAI,CAAC;IACb,8FAA8F;IAC9F,QAAQ,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC;CACjC;AAQD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAsDlE"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scheduler.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.test.d.ts","sourceRoot":"","sources":["../../src/scheduler/scheduler.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,19 @@
1
+ import type { ChannelSenderRegistry } from "../channels/senders";
2
+ import type { ComposeFn, ScheduleClaimStore, ScheduleEntry, TickResult } from "./types";
3
+ export interface TickOptions {
4
+ claims: ScheduleClaimStore;
5
+ senders: ChannelSenderRegistry;
6
+ now: number;
7
+ graceMs: number;
8
+ fallbackMessage: string;
9
+ compose?: ComposeFn;
10
+ voiceAttempted?: boolean;
11
+ }
12
+ /**
13
+ * One scheduler tick over caller-supplied candidate entries. Chatter stores
14
+ * no schedule content of its own — `entries` is whatever the caller's own
15
+ * store considers pending; this adds the exactly-once claim, the grace
16
+ * window, and delivery on top.
17
+ */
18
+ export declare function runTick(entries: ScheduleEntry[], options: TickOptions): Promise<TickResult>;
19
+ //# sourceMappingURL=tick.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tick.d.ts","sourceRoot":"","sources":["../../src/scheduler/tick.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAIjE,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAExF,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,OAAO,EAAE,qBAAqB,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;GAKG;AACH,wBAAsB,OAAO,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAsCjG"}