@diegoaltoworks/chatter 0.34.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -0
- package/dist/index.mjs +35 -0
- package/dist/scheduler/claimStore.d.ts +27 -0
- package/dist/scheduler/claimStore.d.ts.map +1 -0
- package/dist/scheduler/claimStore.test.d.ts +2 -0
- package/dist/scheduler/claimStore.test.d.ts.map +1 -0
- package/dist/scheduler/compose.d.ts +23 -0
- package/dist/scheduler/compose.d.ts.map +1 -0
- package/dist/scheduler/compose.test.d.ts +2 -0
- package/dist/scheduler/compose.test.d.ts.map +1 -0
- package/dist/scheduler/deliver.d.ts +10 -0
- package/dist/scheduler/deliver.d.ts.map +1 -0
- package/dist/scheduler/deliver.test.d.ts +2 -0
- package/dist/scheduler/deliver.test.d.ts.map +1 -0
- package/dist/scheduler/dueDrops.d.ts +13 -0
- package/dist/scheduler/dueDrops.d.ts.map +1 -0
- package/dist/scheduler/dueDrops.test.d.ts +2 -0
- package/dist/scheduler/dueDrops.test.d.ts.map +1 -0
- package/dist/scheduler/index.d.ts +31 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +332 -0
- package/dist/scheduler/index.mjs +290 -0
- package/dist/scheduler/scheduler.d.ts +44 -0
- package/dist/scheduler/scheduler.d.ts.map +1 -0
- package/dist/scheduler/scheduler.test.d.ts +2 -0
- package/dist/scheduler/scheduler.test.d.ts.map +1 -0
- package/dist/scheduler/tick.d.ts +19 -0
- package/dist/scheduler/tick.d.ts.map +1 -0
- package/dist/scheduler/tick.test.d.ts +2 -0
- package/dist/scheduler/tick.test.d.ts.map +1 -0
- package/dist/scheduler/types.d.ts +36 -0
- package/dist/scheduler/types.d.ts.map +1 -0
- package/dist/shortener.d.ts +24 -0
- package/dist/shortener.d.ts.map +1 -0
- package/dist/shortener.test.d.ts +2 -0
- package/dist/shortener.test.d.ts.map +1 -0
- package/package.json +8 -2
package/dist/index.d.ts
CHANGED
|
@@ -42,5 +42,7 @@ export { privateRoutes } from "./routes/private";
|
|
|
42
42
|
export { publicRoutes } from "./routes/public";
|
|
43
43
|
export type { ChatterApp } from "./server";
|
|
44
44
|
export { createServer } from "./server";
|
|
45
|
+
export type { Shortener, ShortenerConfig } from "./shortener";
|
|
46
|
+
export { createShortener } from "./shortener";
|
|
45
47
|
export type { BotBranding, BotChatConfig, BotIdentity, ChatterConfig, CustomRoutes, EmbeddingChunk, KnowledgeDocument, ServerDependencies, } from "./types";
|
|
46
48
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAIvC,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,QAAQ,EACR,eAAe,GAChB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACrD,YAAY,EACV,QAAQ,EACR,aAAa,EACb,cAAc,EACd,aAAa,EACb,WAAW,GACZ,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACzD,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEnF,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEvE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAG/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEzE,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAIvC,YAAY,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,WAAW,EACX,QAAQ,EACR,eAAe,GAChB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACrD,YAAY,EACV,QAAQ,EACR,aAAa,EACb,cAAc,EACd,aAAa,EACb,WAAW,GACZ,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AACzD,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAEpE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC5E,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEnF,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,eAAe,GAChB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEvE,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAG/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,YAAY,EACV,WAAW,EACX,aAAa,EACb,WAAW,EACX,aAAa,EACb,YAAY,EACZ,cAAc,EACd,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -7550,6 +7550,7 @@ __export(index_exports, {
|
|
|
7550
7550
|
createServer: () => createServer,
|
|
7551
7551
|
createSession: () => createSession,
|
|
7552
7552
|
createSessionMiddleware: () => createSessionMiddleware,
|
|
7553
|
+
createShortener: () => createShortener,
|
|
7553
7554
|
defaultBuckets: () => defaultBuckets,
|
|
7554
7555
|
demoRoutes: () => demoRoutes,
|
|
7555
7556
|
detectLeakage: () => detectLeakage,
|
|
@@ -33396,6 +33397,40 @@ async function createServer(config2) {
|
|
|
33396
33397
|
}
|
|
33397
33398
|
});
|
|
33398
33399
|
}
|
|
33400
|
+
|
|
33401
|
+
// src/shortener.ts
|
|
33402
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
33403
|
+
function createShortener(config2, fetchImpl = fetch) {
|
|
33404
|
+
const apiUrl = config2.apiUrl.replace(/\/+$/, "");
|
|
33405
|
+
return {
|
|
33406
|
+
async shorten(url2) {
|
|
33407
|
+
const controller = new AbortController();
|
|
33408
|
+
const timeout = setTimeout(() => controller.abort(), config2.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
33409
|
+
try {
|
|
33410
|
+
const response = await fetchImpl(`${apiUrl}/api/v2/links`, {
|
|
33411
|
+
method: "POST",
|
|
33412
|
+
headers: {
|
|
33413
|
+
"Content-Type": "application/json",
|
|
33414
|
+
"X-API-Key": config2.apiKey
|
|
33415
|
+
},
|
|
33416
|
+
body: JSON.stringify({ target: url2, domain: config2.domain }),
|
|
33417
|
+
signal: controller.signal
|
|
33418
|
+
});
|
|
33419
|
+
if (!response.ok) {
|
|
33420
|
+
await response.text().catch(() => "");
|
|
33421
|
+
return url2;
|
|
33422
|
+
}
|
|
33423
|
+
const data = await response.json();
|
|
33424
|
+
if (typeof data.link !== "string") return url2;
|
|
33425
|
+
return data.link.replace(/^http:\/\//, "https://");
|
|
33426
|
+
} catch {
|
|
33427
|
+
return url2;
|
|
33428
|
+
} finally {
|
|
33429
|
+
clearTimeout(timeout);
|
|
33430
|
+
}
|
|
33431
|
+
}
|
|
33432
|
+
};
|
|
33433
|
+
}
|
|
33399
33434
|
// Annotate the CommonJS export names for ESM import in node:
|
|
33400
33435
|
0 && (module.exports = {
|
|
33401
33436
|
ApiKeyManager,
|
|
@@ -33418,6 +33453,7 @@ async function createServer(config2) {
|
|
|
33418
33453
|
createServer,
|
|
33419
33454
|
createSession,
|
|
33420
33455
|
createSessionMiddleware,
|
|
33456
|
+
createShortener,
|
|
33421
33457
|
defaultBuckets,
|
|
33422
33458
|
demoRoutes,
|
|
33423
33459
|
detectLeakage,
|
package/dist/index.mjs
CHANGED
|
@@ -33358,6 +33358,40 @@ async function createServer(config2) {
|
|
|
33358
33358
|
}
|
|
33359
33359
|
});
|
|
33360
33360
|
}
|
|
33361
|
+
|
|
33362
|
+
// src/shortener.ts
|
|
33363
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
33364
|
+
function createShortener(config2, fetchImpl = fetch) {
|
|
33365
|
+
const apiUrl = config2.apiUrl.replace(/\/+$/, "");
|
|
33366
|
+
return {
|
|
33367
|
+
async shorten(url2) {
|
|
33368
|
+
const controller = new AbortController();
|
|
33369
|
+
const timeout = setTimeout(() => controller.abort(), config2.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
33370
|
+
try {
|
|
33371
|
+
const response = await fetchImpl(`${apiUrl}/api/v2/links`, {
|
|
33372
|
+
method: "POST",
|
|
33373
|
+
headers: {
|
|
33374
|
+
"Content-Type": "application/json",
|
|
33375
|
+
"X-API-Key": config2.apiKey
|
|
33376
|
+
},
|
|
33377
|
+
body: JSON.stringify({ target: url2, domain: config2.domain }),
|
|
33378
|
+
signal: controller.signal
|
|
33379
|
+
});
|
|
33380
|
+
if (!response.ok) {
|
|
33381
|
+
await response.text().catch(() => "");
|
|
33382
|
+
return url2;
|
|
33383
|
+
}
|
|
33384
|
+
const data = await response.json();
|
|
33385
|
+
if (typeof data.link !== "string") return url2;
|
|
33386
|
+
return data.link.replace(/^http:\/\//, "https://");
|
|
33387
|
+
} catch {
|
|
33388
|
+
return url2;
|
|
33389
|
+
} finally {
|
|
33390
|
+
clearTimeout(timeout);
|
|
33391
|
+
}
|
|
33392
|
+
}
|
|
33393
|
+
};
|
|
33394
|
+
}
|
|
33361
33395
|
var export_Chat = import_client3.Chat;
|
|
33362
33396
|
var export_ChatBot = import_client3.ChatBot;
|
|
33363
33397
|
var export_ChatButton = import_client3.ChatButton;
|
|
@@ -33382,6 +33416,7 @@ export {
|
|
|
33382
33416
|
createServer,
|
|
33383
33417
|
createSession,
|
|
33384
33418
|
createSessionMiddleware,
|
|
33419
|
+
createShortener,
|
|
33385
33420
|
defaultBuckets,
|
|
33386
33421
|
demoRoutes,
|
|
33387
33422
|
detectLeakage,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Turso-backed exactly-once claim: an atomic upsert means two instances
|
|
3
|
+
* racing the same entry can never both believe they claimed it — see
|
|
4
|
+
* `canClaim`, which mirrors `canAcquireLease` in
|
|
5
|
+
* `../channels/whatsapp/lease.ts`. A failed send calls `release` to delete
|
|
6
|
+
* the claim row, letting the entry be claimed again by whichever instance
|
|
7
|
+
* ticks next within the grace window. An instance that dies mid-delivery
|
|
8
|
+
* without releasing leaves its claim to go stale instead: after
|
|
9
|
+
* `claimTimeoutMs` with no renewal, another instance may take it over.
|
|
10
|
+
*/
|
|
11
|
+
import type { Client } from "@libsql/client";
|
|
12
|
+
import type { ScheduleClaimStore } from "./types";
|
|
13
|
+
export declare const DEFAULT_CLAIM_TABLE = "chatter_schedule_claims";
|
|
14
|
+
/** How long an unreleased claim blocks reclaiming — short relative to a typical grace window, so a crashed claimant frees up in time for a retry to still land within grace. */
|
|
15
|
+
export declare const DEFAULT_CLAIM_TIMEOUT_MS: number;
|
|
16
|
+
export interface ScheduleClaimRow {
|
|
17
|
+
instanceId: string;
|
|
18
|
+
claimedAt: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Pure: may `instanceId` take (or keep) the claim on an entry right now,
|
|
22
|
+
* given the current row (null = unclaimed)? Mirrors the SQL WHERE clause in
|
|
23
|
+
* `createTursoScheduleClaimStore` — keep the two in sync.
|
|
24
|
+
*/
|
|
25
|
+
export declare function canClaim(existing: ScheduleClaimRow | null, instanceId: string, now: number, claimTimeoutMs: number): boolean;
|
|
26
|
+
export declare function createTursoScheduleClaimStore(client: Client, instanceId: string, tableName?: string, claimTimeoutMs?: number): ScheduleClaimStore;
|
|
27
|
+
//# sourceMappingURL=claimStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claimStore.d.ts","sourceRoot":"","sources":["../../src/scheduler/claimStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAElD,eAAO,MAAM,mBAAmB,4BAA4B,CAAC;AAC7D,gLAAgL;AAChL,eAAO,MAAM,wBAAwB,QAAa,CAAC;AAKnD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,gBAAgB,GAAG,IAAI,EACjC,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,MAAM,GACrB,OAAO,CAIT;AA8BD,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,SAAS,GAAE,MAA4B,EACvC,cAAc,GAAE,MAAiC,GAChD,kBAAkB,CA4BpB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claimStore.test.d.ts","sourceRoot":"","sources":["../../src/scheduler/claimStore.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type OpenAI from "openai";
|
|
2
|
+
import type { AnswerFn } from "../core/answer";
|
|
3
|
+
import type { PipelineMessage, PipelineMode } from "../core/pipeline";
|
|
4
|
+
import type { ComposeFn, ScheduleEntry } from "./types";
|
|
5
|
+
/**
|
|
6
|
+
* Runs the pluggable compose step and guarantees a message comes back: a
|
|
7
|
+
* missing compose function, a blank result, or a throwing/rejecting compose
|
|
8
|
+
* all fall back to `fallbackMessage` rather than blocking delivery.
|
|
9
|
+
*/
|
|
10
|
+
export declare function composeMessage(entry: ScheduleEntry, compose: ComposeFn | undefined, fallbackMessage: string): Promise<string>;
|
|
11
|
+
export interface AnswerComposeOptions {
|
|
12
|
+
client: OpenAI;
|
|
13
|
+
system: string;
|
|
14
|
+
/** Turns a schedule entry's opaque payload into the messages sent to the brain. */
|
|
15
|
+
buildMessages: (entry: ScheduleEntry) => PipelineMessage[];
|
|
16
|
+
answerFn?: AnswerFn;
|
|
17
|
+
mode?: PipelineMode;
|
|
18
|
+
temperature?: number;
|
|
19
|
+
model?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Builds a `ComposeFn` that composes via `answerFn`/`completeOnce`, for callers who want an LLM-written message instead of static text. */
|
|
22
|
+
export declare function createAnswerCompose(options: AnswerComposeOptions): ComposeFn;
|
|
23
|
+
//# sourceMappingURL=compose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../src/scheduler/compose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAExD;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,SAAS,GAAG,SAAS,EAC9B,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,mFAAmF;IACnF,aAAa,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,eAAe,EAAE,CAAC;IAC3D,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,4IAA4I;AAC5I,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,GAAG,SAAS,CAa5E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose.test.d.ts","sourceRoot":"","sources":["../../src/scheduler/compose.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChannelSenderRegistry } from "../channels/senders";
|
|
2
|
+
import type { ScheduleEntry } from "./types";
|
|
3
|
+
export interface DeliverOptions {
|
|
4
|
+
senders: ChannelSenderRegistry;
|
|
5
|
+
/** Try voice first when the channel supports it; text is always the guaranteed fallback. */
|
|
6
|
+
voiceAttempted?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Delivers a composed message. Text is guaranteed; voice is only ever a best-effort first attempt. */
|
|
9
|
+
export declare function deliver(entry: ScheduleEntry, message: string, options: DeliverOptions): Promise<boolean>;
|
|
10
|
+
//# sourceMappingURL=deliver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliver.d.ts","sourceRoot":"","sources":["../../src/scheduler/deliver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,qBAAqB,CAAC;IAC/B,4FAA4F;IAC5F,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,uGAAuG;AACvG,wBAAsB,OAAO,CAC3B,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,OAAO,CAAC,CAMlB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deliver.test.d.ts","sourceRoot":"","sources":["../../src/scheduler/deliver.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure fire-time filtering. A schedule fires once its time has passed, but
|
|
3
|
+
* only within a grace window — a stale entry (the process was down, or the
|
|
4
|
+
* tick was delayed) past grace is skipped rather than delivered late.
|
|
5
|
+
*/
|
|
6
|
+
import type { ScheduleEntry } from "./types";
|
|
7
|
+
export declare function isDue(entry: ScheduleEntry, now: number): boolean;
|
|
8
|
+
export declare function isWithinGrace(entry: ScheduleEntry, now: number, graceMs: number): boolean;
|
|
9
|
+
/** Due entries still inside their grace window, in caller-supplied order. */
|
|
10
|
+
export declare function dueDrops(entries: ScheduleEntry[], now: number, graceMs: number): ScheduleEntry[];
|
|
11
|
+
/** Due entries whose grace window has already passed — reported so callers can log them, never delivered. */
|
|
12
|
+
export declare function staleDrops(entries: ScheduleEntry[], now: number, graceMs: number): ScheduleEntry[];
|
|
13
|
+
//# sourceMappingURL=dueDrops.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dueDrops.d.ts","sourceRoot":"","sources":["../../src/scheduler/dueDrops.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,wBAAgB,KAAK,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAEzF;AAED,6EAA6E;AAC7E,wBAAgB,QAAQ,CAAC,OAAO,EAAE,aAAa,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,aAAa,EAAE,CAEhG;AAED,6GAA6G;AAC7G,wBAAgB,UAAU,CACxB,OAAO,EAAE,aAAa,EAAE,EACxB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,GACd,aAAa,EAAE,CAEjB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dueDrops.test.d.ts","sourceRoot":"","sources":["../../src/scheduler/dueDrops.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scheduler — exactly-once outbound delivery on a caller-supplied fire time.
|
|
3
|
+
* Published as the `@diegoaltoworks/chatter/scheduler` subpath so the core
|
|
4
|
+
* install stays untouched by it.
|
|
5
|
+
*
|
|
6
|
+
* Content-free: chatter stores no schedule data of its own. A caller
|
|
7
|
+
* supplies candidate {@link ScheduleEntry} objects each tick (its own store,
|
|
8
|
+
* cron, queue, anything); this module adds the parts that are easy to get
|
|
9
|
+
* wrong in a multi-instance deployment — an exactly-once claim in Turso so
|
|
10
|
+
* two instances never both deliver the same entry, a fire-time grace window
|
|
11
|
+
* so a delayed tick doesn't send stale messages, a pluggable compose step
|
|
12
|
+
* (static text or an LLM call via `answerFn`/`completeOnce`) that always
|
|
13
|
+
* falls back rather than blocking delivery, and delivery through the
|
|
14
|
+
* channel sender registry with an optional voice-attempted/text-guaranteed
|
|
15
|
+
* mode.
|
|
16
|
+
*
|
|
17
|
+
* @packageDocumentation
|
|
18
|
+
*/
|
|
19
|
+
export type { ScheduleClaimRow } from "./claimStore";
|
|
20
|
+
export { canClaim, createTursoScheduleClaimStore, DEFAULT_CLAIM_TABLE, DEFAULT_CLAIM_TIMEOUT_MS, } from "./claimStore";
|
|
21
|
+
export type { AnswerComposeOptions } from "./compose";
|
|
22
|
+
export { composeMessage, createAnswerCompose } from "./compose";
|
|
23
|
+
export type { DeliverOptions } from "./deliver";
|
|
24
|
+
export { deliver } from "./deliver";
|
|
25
|
+
export { dueDrops, isDue, isWithinGrace, staleDrops } from "./dueDrops";
|
|
26
|
+
export type { Scheduler, SchedulerConfig } from "./scheduler";
|
|
27
|
+
export { createScheduler, DEFAULT_FALLBACK_MESSAGE, DEFAULT_GRACE_MS, DEFAULT_INTERVAL_MS, } from "./scheduler";
|
|
28
|
+
export type { TickOptions } from "./tick";
|
|
29
|
+
export { runTick } from "./tick";
|
|
30
|
+
export type { ComposeFn, ScheduleClaimStore, ScheduleEntry, TickResult } from "./types";
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scheduler/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EACL,QAAQ,EACR,6BAA6B,EAC7B,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChE,YAAY,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxE,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,EACL,eAAe,EACf,wBAAwB,EACxB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACjC,YAAY,EAAE,SAAS,EAAE,kBAAkB,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -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 @@
|
|
|
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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tick.test.d.ts","sourceRoot":"","sources":["../../src/scheduler/tick.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-free by design: chatter stores no schedule data of its own.
|
|
3
|
+
* `id`/`fireAt`/`channel`/`chatId` are the minimum the claim/grace/delivery
|
|
4
|
+
* machinery needs; `payload` is opaque caller data for the compose step.
|
|
5
|
+
*/
|
|
6
|
+
export interface ScheduleEntry {
|
|
7
|
+
/** Stable, caller-assigned id — the claim table's primary key. */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Epoch ms the entry is due to fire. */
|
|
10
|
+
fireAt: number;
|
|
11
|
+
/** Sender-registry channel name to deliver through. */
|
|
12
|
+
channel: string;
|
|
13
|
+
/** Channel-specific recipient id. */
|
|
14
|
+
chatId: string;
|
|
15
|
+
/** Opaque caller data passed to the compose step; chatter never reads it. */
|
|
16
|
+
payload?: unknown;
|
|
17
|
+
}
|
|
18
|
+
/** Turns an entry into outbound text. Chatter never inspects the result beyond checking it is non-blank. */
|
|
19
|
+
export type ComposeFn = (entry: ScheduleEntry) => Promise<string> | string;
|
|
20
|
+
export interface ScheduleClaimStore {
|
|
21
|
+
/** Atomically claims `id` for this instance; resolves false if another instance already holds it. */
|
|
22
|
+
claim(id: string, now: number): Promise<boolean>;
|
|
23
|
+
/** Releases this instance's claim on `id`, e.g. after a failed send, so it can be retried within grace. */
|
|
24
|
+
release(id: string): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export interface TickResult {
|
|
27
|
+
/** Ids delivered this tick. */
|
|
28
|
+
sent: string[];
|
|
29
|
+
/** Ids due within grace but already claimed by another instance. */
|
|
30
|
+
notClaimed: string[];
|
|
31
|
+
/** Ids due but past their grace window — skipped, never claimed or delivered. */
|
|
32
|
+
stale: string[];
|
|
33
|
+
/** Ids claimed this tick whose delivery failed; the claim was released so a later tick can retry. */
|
|
34
|
+
failed: string[];
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/scheduler/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,yCAAyC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,4GAA4G;AAC5G,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;AAE3E,MAAM,WAAW,kBAAkB;IACjC,qGAAqG;IACrG,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjD,2GAA2G;IAC3G,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,UAAU;IACzB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,oEAAoE;IACpE,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,iFAAiF;IACjF,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,qGAAqG;IACrG,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL shortening against a Kutt-compatible API, config-injected (no
|
|
3
|
+
* `process.env` reads) so hosts can wire credentials however they like.
|
|
4
|
+
*
|
|
5
|
+
* The hard invariant: {@link Shortener.shorten} never throws. Any failure —
|
|
6
|
+
* network error, an aborted request past `timeoutMs`, or a non-2xx/malformed
|
|
7
|
+
* response — resolves to the original URL, since a shortener outage must
|
|
8
|
+
* never break something user-facing that only wanted a shorter link.
|
|
9
|
+
*/
|
|
10
|
+
export interface ShortenerConfig {
|
|
11
|
+
/** Base URL of the Kutt (or Kutt-compatible) instance, e.g. "https://kutt.it". */
|
|
12
|
+
apiUrl: string;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
/** Custom short domain configured on the Kutt instance, if any. */
|
|
15
|
+
domain?: string;
|
|
16
|
+
/** Abort the request after this many milliseconds. Defaults to 5000. */
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface Shortener {
|
|
20
|
+
/** Resolves to the shortened URL, or the input `url` unchanged on any failure. */
|
|
21
|
+
shorten(url: string): Promise<string>;
|
|
22
|
+
}
|
|
23
|
+
export declare function createShortener(config: ShortenerConfig, fetchImpl?: typeof fetch): Shortener;
|
|
24
|
+
//# sourceMappingURL=shortener.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shortener.d.ts","sourceRoot":"","sources":["../src/shortener.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,eAAe;IAC9B,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,SAAS;IACxB,kFAAkF;IAClF,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACvC;AAQD,wBAAgB,eAAe,CAC7B,MAAM,EAAE,eAAe,EACvB,SAAS,GAAE,OAAO,KAAa,GAC9B,SAAS,CAqCX"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shortener.test.d.ts","sourceRoot":"","sources":["../src/shortener.test.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegoaltoworks/chatter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Embeddable AI chatbot framework with RAG, authentication, and customizable widgets",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -59,6 +59,11 @@
|
|
|
59
59
|
"types": "./dist/images/index.d.ts",
|
|
60
60
|
"import": "./dist/images/index.mjs",
|
|
61
61
|
"require": "./dist/images/index.js"
|
|
62
|
+
},
|
|
63
|
+
"./scheduler": {
|
|
64
|
+
"types": "./dist/scheduler/index.d.ts",
|
|
65
|
+
"import": "./dist/scheduler/index.mjs",
|
|
66
|
+
"require": "./dist/scheduler/index.js"
|
|
62
67
|
}
|
|
63
68
|
},
|
|
64
69
|
"files": [
|
|
@@ -68,7 +73,7 @@
|
|
|
68
73
|
],
|
|
69
74
|
"scripts": {
|
|
70
75
|
"dev": "bun --watch src/index.ts",
|
|
71
|
-
"build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs && bun run build:mcp && bun run build:usage && bun run build:personas && bun run build:channels && bun run build:whatsapp && bun run build:flows && bun run build:images && bun run build:bin && bun run build:client && bun run build:copy-client",
|
|
76
|
+
"build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs && bun run build:mcp && bun run build:usage && bun run build:personas && bun run build:channels && bun run build:whatsapp && bun run build:flows && bun run build:images && bun run build:scheduler && bun run build:bin && bun run build:client && bun run build:copy-client",
|
|
72
77
|
"build:clean": "rm -rf dist",
|
|
73
78
|
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
|
74
79
|
"build:esm": "esbuild src/index.ts --bundle --format=esm --outfile=dist/index.mjs --external:hono --external:openai --external:@libsql/client --external:jose --platform=node --target=node24",
|
|
@@ -80,6 +85,7 @@
|
|
|
80
85
|
"build:whatsapp": "esbuild src/channels/whatsapp/index.ts --bundle --format=esm --outfile=dist/channels/whatsapp/index.mjs --external:@libsql/client --external:@whiskeysockets/baileys --platform=node --target=node24 && esbuild src/channels/whatsapp/index.ts --bundle --format=cjs --outfile=dist/channels/whatsapp/index.js --external:@libsql/client --external:@whiskeysockets/baileys --platform=node --target=node24",
|
|
81
86
|
"build:flows": "esbuild src/flows/index.ts --bundle --format=esm --outfile=dist/flows/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/flows/index.ts --bundle --format=cjs --outfile=dist/flows/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
|
|
82
87
|
"build:images": "esbuild src/images/index.ts --bundle --format=esm --outfile=dist/images/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/images/index.ts --bundle --format=cjs --outfile=dist/images/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
|
|
88
|
+
"build:scheduler": "esbuild src/scheduler/index.ts --bundle --format=esm --outfile=dist/scheduler/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/scheduler/index.ts --bundle --format=cjs --outfile=dist/scheduler/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
|
|
83
89
|
"build:bin": "esbuild bin/create-apikey.ts --bundle --format=cjs --outfile=dist/bin/create-apikey.js --external:jose --platform=node --target=node18 --banner:js='#!/usr/bin/env node' && esbuild bin/wa-pair.ts --bundle --format=cjs --outfile=dist/bin/wa-pair.js --external:@libsql/client --external:@whiskeysockets/baileys --external:qrcode-terminal --platform=node --target=node18 --banner:js='#!/usr/bin/env node'",
|
|
84
90
|
"build:client": "cd src/client && bun run build",
|
|
85
91
|
"build:copy-client": "mkdir -p dist/static && cp src/client/dist/chatter.js src/client/dist/chatter.css dist/static/",
|