cognova 0.2.8 → 0.2.10
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/.output/nitro.json +1 -1
- package/.output/public/_nuxt/{CH_jROT9.js → 0NJ3PaRM.js} +1 -1
- package/.output/public/_nuxt/{C075hYlX.js → 0yk-pS3R.js} +2 -2
- package/.output/public/_nuxt/{CVYcsqmL.js → 10_wwHSz.js} +1 -1
- package/.output/public/_nuxt/{BUC36BHp.js → 1zUTf4AP.js} +1 -1
- package/.output/public/_nuxt/{njnzCNrH.js → 2dNDtTiv.js} +3 -3
- package/.output/public/_nuxt/{B8TQvzYX.js → 2nOqGUPr.js} +1 -1
- package/.output/public/_nuxt/{ijmD2cEW.js → 3jQMk_5H.js} +1 -1
- package/.output/public/_nuxt/{D4X6ONPF.js → 4K03TkGA.js} +1 -1
- package/.output/public/_nuxt/{C3tximD1.js → 6boMs_nF.js} +1 -1
- package/.output/public/_nuxt/{Drxf7nJz.js → 7oCGSglN.js} +1 -1
- package/.output/public/_nuxt/{Dt5mOzXj.js → 8q5NepGW.js} +1 -1
- package/.output/public/_nuxt/{GYf-vnR6.js → B93pdGAW.js} +1 -1
- package/.output/public/_nuxt/{BZBNJD7U.js → BA_pRWwX.js} +1 -1
- package/.output/public/_nuxt/{eE8TtXgC.js → BDyn4ApB.js} +3 -3
- package/.output/public/_nuxt/{0dqQoCHY.js → BGgwYWjH.js} +1 -1
- package/.output/public/_nuxt/{BrOKokPO.js → BH0QDWEm.js} +1 -1
- package/.output/public/_nuxt/{B5SnZL2l.js → BHtY0l0l.js} +1 -1
- package/.output/public/_nuxt/{ImPsUDiH.js → BIXrSYwR.js} +1 -1
- package/.output/public/_nuxt/BK8S2ony.js +1 -0
- package/.output/public/_nuxt/{Bkc1Hpcf.js → BKXg-alD.js} +1 -1
- package/.output/public/_nuxt/{BahXyh-A.js → BLnYhy_t.js} +1 -1
- package/.output/public/_nuxt/{Bhhg7oSf.js → BR8bKz8h.js} +1 -1
- package/.output/public/_nuxt/{D2kQkOaG.js → BS2ZNXI1.js} +1 -1
- package/.output/public/_nuxt/{BLm0UviR.js → BUghTae1.js} +1 -1
- package/.output/public/_nuxt/{D4_89MZm.js → BXuWCWsJ.js} +1 -1
- package/.output/public/_nuxt/{LQNRVThr.js → BYHCP8x7.js} +1 -1
- package/.output/public/_nuxt/{BYtVdUIW.js → BZrSPX0S.js} +1 -1
- package/.output/public/_nuxt/{U3f6dy8i.js → B_wilgcd.js} +1 -1
- package/.output/public/_nuxt/BaZwjF8o.js +1 -0
- package/.output/public/_nuxt/{BefOhWHD.js → Bciqk4dX.js} +1 -1
- package/.output/public/_nuxt/{CewcVxDS.js → Beom-INj.js} +1 -1
- package/.output/public/_nuxt/{BlsIfvSf.js → BfcLZ_fC.js} +1 -1
- package/.output/public/_nuxt/{dB7G4iIC.js → Bk6JUtIo.js} +1 -1
- package/.output/public/_nuxt/BkfI94R8.js +1 -0
- package/.output/public/_nuxt/{DqzomZoo.js → BmOtR1wX.js} +1 -1
- package/.output/public/_nuxt/{4XnyUc7C.js → BmVLxnDU.js} +1 -1
- package/.output/public/_nuxt/{DtJnWtpL.js → BtHQ1T0f.js} +1 -1
- package/.output/public/_nuxt/{DcbbfwCe.js → ByYh0uRg.js} +1 -1
- package/.output/public/_nuxt/{CbvNJVrk.js → C27WoGNZ.js} +1 -1
- package/.output/public/_nuxt/{CY7f553E.js → C2FxZbO_.js} +1 -1
- package/.output/public/_nuxt/{zO9MgHj2.js → C2vq6Te8.js} +1 -1
- package/.output/public/_nuxt/{BXtZVbmx.js → C39dQ88F.js} +1 -1
- package/.output/public/_nuxt/{DmDPSvxi.js → C3NST0BU.js} +1 -1
- package/.output/public/_nuxt/{nHSiDk8N.js → C4mwL7SE.js} +1 -1
- package/.output/public/_nuxt/{BBDxhFYY.js → C5R5QaCA.js} +1 -1
- package/.output/public/_nuxt/{D79lbuWG.js → C6qKcgOY.js} +1 -1
- package/.output/public/_nuxt/{C-HCArYP.js → C71_a1IX.js} +1 -1
- package/.output/public/_nuxt/{CSjOaBKu.js → C9WIgRRL.js} +1 -1
- package/.output/public/_nuxt/{DmOS3vFH.js → CBTkrk2M.js} +1 -1
- package/.output/public/_nuxt/CEnSeCqn.js +1 -0
- package/.output/public/_nuxt/{CAfyXyeo.js → CKxSHyww.js} +1 -1
- package/.output/public/_nuxt/{D4Cizi4y.js → CLfF6dSn.js} +1 -1
- package/.output/public/_nuxt/{C7qhmmZh.js → CPutXj8l.js} +1 -1
- package/.output/public/_nuxt/{BCCYopTB.js → CQqCBrXj.js} +1 -1
- package/.output/public/_nuxt/{D-Ge96eV.js → CSKJ-Ahh.js} +1 -1
- package/.output/public/_nuxt/{B2Xfh7am.js → CTCcEJU3.js} +1 -1
- package/.output/public/_nuxt/{BnJ-6YDw.js → CVFwOzbl.js} +1 -1
- package/.output/public/_nuxt/{C21BjagS.js → CVdCqaby.js} +1 -1
- package/.output/public/_nuxt/{2QHXgMyi.js → CVqlefTY.js} +1 -1
- package/.output/public/_nuxt/CWyMCJQH.js +1 -0
- package/.output/public/_nuxt/{B6-U3KqJ.js → CY-QVcA5.js} +2 -2
- package/.output/public/_nuxt/{Cr0NfhPB.js → CY-cjAwJ.js} +1 -1
- package/.output/public/_nuxt/{Bwn-CRYH.js → CYPO5o_C.js} +1 -1
- package/.output/public/_nuxt/{B-AlyT2D.js → CZRnNmU8.js} +1 -1
- package/.output/public/_nuxt/{DwIYtFfA.js → CZVzFlpI.js} +1 -1
- package/.output/public/_nuxt/{BkO884Ds.js → CZoEPC_Q.js} +3 -3
- package/.output/public/_nuxt/{BJUff9Ku.js → Cc-2ziaZ.js} +1 -1
- package/.output/public/_nuxt/{BLwctszt.js → ChcO9s3o.js} +1 -1
- package/.output/public/_nuxt/{DX_9SfFb.js → Cl-LOSDV.js} +1 -1
- package/.output/public/_nuxt/{CSHcfejh.js → CobqYwkp.js} +1 -1
- package/.output/public/_nuxt/{B0xuHXvN.js → CqNwHCpo.js} +1 -1
- package/.output/public/_nuxt/{DMb_1D81.js → CuxrHsu-.js} +1 -1
- package/.output/public/_nuxt/{GGcn5KPW.js → CwPdIZ9J.js} +1 -1
- package/.output/public/_nuxt/{D7pKT3IW.js → D0ifH682.js} +1 -1
- package/.output/public/_nuxt/{KEeoKtq_.js → D3AZaldL.js} +1 -1
- package/.output/public/_nuxt/{BdxP21ep.js → D4UJwDQJ.js} +1 -1
- package/.output/public/_nuxt/{C-Ygk9x5.js → D5jZq8b3.js} +1 -1
- package/.output/public/_nuxt/{B6G3NLfW.js → D5q8SnqE.js} +1 -1
- package/.output/public/_nuxt/{CDkyQ1eQ.js → D8M722pn.js} +1 -1
- package/.output/public/_nuxt/{BjnQ1pI1.js → D8lwrAYS.js} +1 -1
- package/.output/public/_nuxt/{C13FKhqK.js → D9nmzBAw.js} +1 -1
- package/.output/public/_nuxt/{ClGn48iq.js → DC4idAGt.js} +1 -1
- package/.output/public/_nuxt/{vES4kgx7.js → DEd2xVbS.js} +1 -1
- package/.output/public/_nuxt/{CClsKJ9A.js → DH4YkDAi.js} +1 -1
- package/.output/public/_nuxt/{D-5A6cMB.js → DJTCT0bl.js} +1 -1
- package/.output/public/_nuxt/{D_O7SlHm.js → DNP5E1bC.js} +1 -1
- package/.output/public/_nuxt/{BF3tVKtX.js → DO9SFIh1.js} +1 -1
- package/.output/public/_nuxt/DPklr_tJ.js +2 -0
- package/.output/public/_nuxt/{mfAEx0Kz.js → DQM4eBdt.js} +1 -1
- package/.output/public/_nuxt/{cPPThz84.js → DQh6I9z9.js} +1 -1
- package/.output/public/_nuxt/{CXTrUfQ5.js → DS2wStH1.js} +1 -1
- package/.output/public/_nuxt/{CYQP4zpi.js → DTAStixR.js} +1 -1
- package/.output/public/_nuxt/{CEz288t8.js → DTGenhcA.js} +1 -1
- package/.output/public/_nuxt/{B9zgEO8N.js → DUAAXJ6Q.js} +1 -1
- package/.output/public/_nuxt/{CzAZpALW.js → DUVzIl3o.js} +1 -1
- package/.output/public/_nuxt/{DJt7Rogf.js → DXdwpJ-I.js} +1 -1
- package/.output/public/_nuxt/{MWUW1sIF.js → DY3uK7wM.js} +1 -1
- package/.output/public/_nuxt/{CfY_bsV0.js → DYbZBZet.js} +1 -1
- package/.output/public/_nuxt/{DZEuR9ms.js → D_4LPm1U.js} +1 -1
- package/.output/public/_nuxt/{CrmE-JGu.js → Db4KMnt8.js} +1 -1
- package/.output/public/_nuxt/{BwC62XRN.js → DcJbYLTp.js} +1 -1
- package/.output/public/_nuxt/{C6sX7TYf.js → DfF81NlA.js} +1 -1
- package/.output/public/_nuxt/{BHn8tRjg.js → DhI5cA_n.js} +1 -1
- package/.output/public/_nuxt/{CQ3tAemj.js → DhuOKJda.js} +1 -1
- package/.output/public/_nuxt/{D887s9k9.js → Di-Nc75e.js} +1 -1
- package/.output/public/_nuxt/{D_obX2Hj.js → Djs0Tlpa.js} +1 -1
- package/.output/public/_nuxt/DmmdPt_5.js +1 -0
- package/.output/public/_nuxt/{DKKY8zWm.js → Dn5a-guE.js} +1 -1
- package/.output/public/_nuxt/{BvstjGHC.js → DtjjnHnt.js} +1 -1
- package/.output/public/_nuxt/{Dbfwn2zk.js → DuvzM-P1.js} +1 -1
- package/.output/public/_nuxt/{BxShcW0K.js → DwY7rCd_.js} +1 -1
- package/.output/public/_nuxt/{BmaHzvZM.js → Dya5oK8u.js} +1 -1
- package/.output/public/_nuxt/{CO3hceDE.js → DzA58_Lm.js} +1 -1
- package/.output/public/_nuxt/{CF33v_m5.js → DzGy77Vr.js} +1 -1
- package/.output/public/_nuxt/{DmlZSBjF.js → E3rXPwU8.js} +1 -1
- package/.output/public/_nuxt/{CEDw8osP.js → EgKnQnf-.js} +1 -1
- package/.output/public/_nuxt/{it8syoZI.js → GtEM7xVU.js} +1 -1
- package/.output/public/_nuxt/{DLUvsixu.js → H6JbrRBU.js} +1 -1
- package/.output/public/_nuxt/{BE1CVWts.js → ILEvizzp.js} +1 -1
- package/.output/public/_nuxt/{BSA-U0so.js → JX1oqJI9.js} +1 -1
- package/.output/public/_nuxt/{yJ9vRvHj.js → JbHa4oXq.js} +1 -1
- package/.output/public/_nuxt/{zXvvpsZj.js → Kw0zy3FG.js} +1 -1
- package/.output/public/_nuxt/{C6FFW_cO.js → N5XtbYVD.js} +1 -1
- package/.output/public/_nuxt/{D04CyXmf.js → PP_4ebzl.js} +1 -1
- package/.output/public/_nuxt/{6QDVg5TO.js → SrncdpaW.js} +1 -1
- package/.output/public/_nuxt/{36ZHqDPo.js → U1MWjQMi.js} +1 -1
- package/.output/public/_nuxt/{mT7Mt2d7.js → Um1vPiAz.js} +1 -1
- package/.output/public/_nuxt/{CqFcWBkW.js → XCjS70z4.js} +1 -1
- package/.output/public/_nuxt/{C24so3m7.js → YX8avsvq.js} +2 -2
- package/.output/public/_nuxt/{DaBSQDif.js → _cy8R3nk.js} +1 -1
- package/.output/public/_nuxt/{D9aYkNhQ.js → apYB9dr5.js} +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/def74b99-d70c-4f30-aa29-70248cbeac7d.json +1 -0
- package/.output/public/_nuxt/entry.NKPfH2kE.css +1 -0
- package/.output/public/_nuxt/{BqTdsCnW.js → fbyIeNkc.js} +1 -1
- package/.output/public/_nuxt/{-2AbaFNH.js → g5MjDvm5.js} +1 -1
- package/.output/public/_nuxt/{C1tCdK-M.js → gTrVszwd.js} +1 -1
- package/.output/public/_nuxt/{BVEFmI4I.js → ixlNW2So.js} +1 -1
- package/.output/public/_nuxt/{DtyyytRx.js → nnQqD5pb.js} +1 -1
- package/.output/public/_nuxt/{pHCJ6lhw.js → rfGRTJJW.js} +1 -1
- package/.output/public/_nuxt/{Buf1PAiO.js → t8aDAkZ5.js} +1 -1
- package/.output/public/_nuxt/{BmdlymIW.js → vIOxcXKR.js} +1 -1
- package/.output/public/_nuxt/{Cve4psva.js → vScW1Zgm.js} +1 -1
- package/.output/public/_nuxt/{SfaNtyKS.js → wO6z2ugJ.js} +1 -1
- package/.output/public/_nuxt/{BhYoxmLV.js → x6FRJ5ac.js} +1 -1
- package/.output/public/_nuxt/{Pn7VD6uQ.js → zq-a1TeT.js} +1 -1
- package/.output/server/chunks/build/{chat-CZMiB68R.mjs → chat-CR3JIVEq.mjs} +65 -11
- package/.output/server/chunks/build/chat-CR3JIVEq.mjs.map +1 -0
- package/.output/server/chunks/build/client.precomputed.mjs +1 -1
- package/.output/server/chunks/build/server.mjs +3 -3
- package/.output/server/chunks/build/{settings-ByUXa46k.mjs → settings-B2KXoGcz.mjs} +213 -26
- package/.output/server/chunks/build/settings-B2KXoGcz.mjs.map +1 -0
- package/.output/server/chunks/build/styles.mjs +2 -2
- package/.output/server/chunks/build/{usage-CSrBh4Or.mjs → usage-BHdQZbfI.mjs} +9 -5
- package/.output/server/chunks/build/{usage-CSrBh4Or.mjs.map → usage-BHdQZbfI.mjs.map} +1 -1
- package/.output/server/chunks/nitro/nitro.mjs +1161 -891
- package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
- package/.output/server/chunks/routes/_ws/chat.mjs +2 -1
- package/.output/server/chunks/routes/_ws/chat.mjs.map +1 -1
- package/.output/server/chunks/routes/api/conversations/_id_.delete.mjs +4 -1
- package/.output/server/chunks/routes/api/conversations/_id_.delete.mjs.map +1 -1
- package/.output/server/chunks/routes/api/index.get3.mjs +1 -1
- package/.output/server/chunks/routes/api/index.get3.mjs.map +1 -1
- package/.output/server/chunks/routes/api/usage/stats.get.mjs +2 -0
- package/.output/server/chunks/routes/api/usage/stats.get.mjs.map +1 -1
- package/.output/server/package.json +1 -1
- package/app/components/chat/ConversationList.vue +7 -1
- package/app/components/chat/MessageBubble.vue +45 -1
- package/app/components/usage/UsageRecordsTable.vue +4 -2
- package/app/components/usage/UsageSourceDonut.client.vue +4 -2
- package/app/components/usage/UsageTopConsumers.vue +4 -2
- package/app/composables/useChat.ts +3 -0
- package/app/pages/settings.vue +148 -9
- package/package.json +1 -1
- package/server/api/conversations/[id].delete.ts +9 -0
- package/server/api/conversations/index.get.ts +2 -1
- package/server/api/usage/stats.get.ts +2 -0
- package/server/bridge/adapters/telegram.ts +83 -8
- package/server/bridge/responder.ts +248 -0
- package/server/bridge/router.ts +6 -4
- package/server/db/schema.ts +3 -1
- package/server/drizzle/migrations/0015_great_mephistopheles.sql +2 -0
- package/server/drizzle/migrations/meta/0015_snapshot.json +1972 -0
- package/server/drizzle/migrations/meta/_journal.json +7 -0
- package/server/routes/_ws/chat.ts +2 -1
- package/server/utils/log-token-usage.ts +1 -1
- package/shared/types/index.ts +6 -0
- package/.output/public/_nuxt/BIo-9u_O.js +0 -1
- package/.output/public/_nuxt/BMs0mHEp.js +0 -1
- package/.output/public/_nuxt/BwyXWqBY.js +0 -1
- package/.output/public/_nuxt/CE9_ECxx.js +0 -1
- package/.output/public/_nuxt/DmcMCO8x.js +0 -1
- package/.output/public/_nuxt/JReYPlwF.js +0 -2
- package/.output/public/_nuxt/builds/meta/410e449d-9fe8-43a0-ae07-80797693b9b8.json +0 -1
- package/.output/public/_nuxt/entry._7ZkP07A.css +0 -1
- package/.output/public/_nuxt/iBaivqi8.js +0 -1
- package/.output/server/chunks/build/chat-CZMiB68R.mjs.map +0 -1
- package/.output/server/chunks/build/settings-ByUXa46k.mjs.map +0 -1
|
@@ -8,9 +8,10 @@ export default defineEventHandler(async (event) => {
|
|
|
8
8
|
|
|
9
9
|
const db = getDb()
|
|
10
10
|
|
|
11
|
+
// Main Chat always first, then by most recent
|
|
11
12
|
const conversations = await db.select()
|
|
12
13
|
.from(schema.conversations)
|
|
13
|
-
.orderBy(desc(schema.conversations.startedAt))
|
|
14
|
+
.orderBy(desc(schema.conversations.isMain), desc(schema.conversations.startedAt))
|
|
14
15
|
.limit(50)
|
|
15
16
|
|
|
16
17
|
return { data: conversations }
|
|
@@ -71,6 +71,7 @@ export default defineEventHandler(async (event) => {
|
|
|
71
71
|
chat: 0,
|
|
72
72
|
agent: 0,
|
|
73
73
|
memory: 0,
|
|
74
|
+
bridge: 0,
|
|
74
75
|
totalCost: 0,
|
|
75
76
|
inputTokens: 0,
|
|
76
77
|
outputTokens: 0,
|
|
@@ -83,6 +84,7 @@ export default defineEventHandler(async (event) => {
|
|
|
83
84
|
if (r.source === 'chat') existing.chat += r.costUsd
|
|
84
85
|
else if (r.source === 'agent') existing.agent += r.costUsd
|
|
85
86
|
else if (r.source === 'memory_extraction') existing.memory += r.costUsd
|
|
87
|
+
else if (r.source === 'bridge') existing.bridge += r.costUsd
|
|
86
88
|
bucketMap.set(bucket, existing)
|
|
87
89
|
|
|
88
90
|
// By source
|
|
@@ -56,10 +56,11 @@ interface TelegramDocument {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* Telegram adapter
|
|
59
|
+
* Telegram adapter supporting both webhook and long-polling modes.
|
|
60
|
+
*
|
|
61
|
+
* - If a public APP_URL is configured, registers a webhook with Telegram.
|
|
62
|
+
* - Otherwise, falls back to long-polling via getUpdates.
|
|
60
63
|
*
|
|
61
|
-
* When started, registers a webhook with Telegram so they push updates to us.
|
|
62
|
-
* When stopped, removes the webhook.
|
|
63
64
|
* Messages are sent via the Bot API sendMessage endpoint.
|
|
64
65
|
*/
|
|
65
66
|
export class TelegramAdapter implements BridgeAdapter {
|
|
@@ -70,6 +71,9 @@ export class TelegramAdapter implements BridgeAdapter {
|
|
|
70
71
|
private botToken: string | null = null
|
|
71
72
|
private webhookSecret: string
|
|
72
73
|
private healthy = false
|
|
74
|
+
private polling = false
|
|
75
|
+
private pollAbort: AbortController | null = null
|
|
76
|
+
private pollOffset = 0
|
|
73
77
|
|
|
74
78
|
constructor(
|
|
75
79
|
bridgeId: string,
|
|
@@ -92,6 +96,11 @@ export class TelegramAdapter implements BridgeAdapter {
|
|
|
92
96
|
throw new Error('TELEGRAM_BOT_TOKEN secret not found. Add it in Settings → Secrets.')
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
// Also check for APP_URL in secrets store (set via Settings → App)
|
|
100
|
+
const appUrlSecret = await getSecretValue('APP_URL')
|
|
101
|
+
if (appUrlSecret && !process.env.APP_URL)
|
|
102
|
+
process.env.APP_URL = appUrlSecret
|
|
103
|
+
|
|
95
104
|
// Validate token by calling getMe
|
|
96
105
|
const me = await this.apiCall('getMe')
|
|
97
106
|
if (!me.ok) {
|
|
@@ -105,7 +114,7 @@ export class TelegramAdapter implements BridgeAdapter {
|
|
|
105
114
|
|
|
106
115
|
console.log(`[telegram] Bot @${me.result?.username} authenticated`)
|
|
107
116
|
|
|
108
|
-
// Register webhook
|
|
117
|
+
// Register webhook or start polling
|
|
109
118
|
const webhookUrl = this.getWebhookUrl()
|
|
110
119
|
if (webhookUrl) {
|
|
111
120
|
const setResult = await this.apiCall('setWebhook', {
|
|
@@ -120,13 +129,19 @@ export class TelegramAdapter implements BridgeAdapter {
|
|
|
120
129
|
|
|
121
130
|
console.log(`[telegram] Webhook registered: ${webhookUrl}`)
|
|
122
131
|
} else {
|
|
123
|
-
|
|
132
|
+
// Delete any existing webhook before polling (Telegram requires this)
|
|
133
|
+
await this.apiCall('deleteWebhook')
|
|
134
|
+
console.log('[telegram] No public URL configured — starting long-polling')
|
|
135
|
+
this.startPolling()
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
this.healthy = true
|
|
127
139
|
}
|
|
128
140
|
|
|
129
141
|
async stop(): Promise<void> {
|
|
142
|
+
// Stop polling loop if running
|
|
143
|
+
this.stopPolling()
|
|
144
|
+
|
|
130
145
|
if (this.botToken) {
|
|
131
146
|
try {
|
|
132
147
|
await this.apiCall('deleteWebhook')
|
|
@@ -238,6 +253,54 @@ export class TelegramAdapter implements BridgeAdapter {
|
|
|
238
253
|
return headerValue === this.webhookSecret
|
|
239
254
|
}
|
|
240
255
|
|
|
256
|
+
// ─── Polling ─────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
private startPolling(): void {
|
|
259
|
+
this.polling = true
|
|
260
|
+
this.pollAbort = new AbortController()
|
|
261
|
+
void this.pollLoop()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private stopPolling(): void {
|
|
265
|
+
this.polling = false
|
|
266
|
+
if (this.pollAbort) {
|
|
267
|
+
this.pollAbort.abort()
|
|
268
|
+
this.pollAbort = null
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private async pollLoop(): Promise<void> {
|
|
273
|
+
while (this.polling && this.botToken) {
|
|
274
|
+
try {
|
|
275
|
+
const result = await this.apiCall('getUpdates', {
|
|
276
|
+
offset: this.pollOffset,
|
|
277
|
+
timeout: 30,
|
|
278
|
+
allowed_updates: ['message', 'edited_message']
|
|
279
|
+
}, this.pollAbort?.signal)
|
|
280
|
+
|
|
281
|
+
if (!this.polling) break
|
|
282
|
+
|
|
283
|
+
if (result.ok && Array.isArray(result.result)) {
|
|
284
|
+
for (const update of result.result as TelegramUpdate[]) {
|
|
285
|
+
this.pollOffset = update.update_id + 1
|
|
286
|
+
await this.handleUpdate(update).catch((error: unknown) => {
|
|
287
|
+
console.error('[telegram] Error handling polled update:', error)
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
} else if (!result.ok) {
|
|
291
|
+
console.error('[telegram] Polling error:', result.description)
|
|
292
|
+
// Back off on error to avoid hammering the API
|
|
293
|
+
await sleep(5000)
|
|
294
|
+
}
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (!this.polling) break
|
|
297
|
+
// Network error — back off and retry
|
|
298
|
+
console.error('[telegram] Poll network error:', error instanceof Error ? error.message : error)
|
|
299
|
+
await sleep(5000)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
241
304
|
// ─── Private helpers ───────────────────────────
|
|
242
305
|
|
|
243
306
|
private getWebhookUrl(): string | null {
|
|
@@ -253,18 +316,25 @@ export class TelegramAdapter implements BridgeAdapter {
|
|
|
253
316
|
return null
|
|
254
317
|
}
|
|
255
318
|
|
|
256
|
-
private async apiCall(
|
|
319
|
+
private async apiCall(
|
|
320
|
+
method: string,
|
|
321
|
+
body?: Record<string, unknown>,
|
|
322
|
+
signal?: AbortSignal
|
|
323
|
+
): Promise<TelegramApiResponse> {
|
|
257
324
|
const url = `${TELEGRAM_API}/bot${this.botToken}/${method}`
|
|
258
325
|
|
|
259
326
|
try {
|
|
260
327
|
const response = await fetch(url, {
|
|
261
328
|
method: 'POST',
|
|
262
329
|
headers: { 'Content-Type': 'application/json' },
|
|
263
|
-
body: body ? JSON.stringify(body) : undefined
|
|
330
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
331
|
+
signal
|
|
264
332
|
})
|
|
265
333
|
|
|
266
334
|
return await response.json() as TelegramApiResponse
|
|
267
335
|
} catch (error) {
|
|
336
|
+
if (error instanceof DOMException && error.name === 'AbortError')
|
|
337
|
+
return { ok: false, description: 'Request aborted' }
|
|
268
338
|
return {
|
|
269
339
|
ok: false,
|
|
270
340
|
description: error instanceof Error ? error.message : 'Network error'
|
|
@@ -283,7 +353,8 @@ export class TelegramAdapter implements BridgeAdapter {
|
|
|
283
353
|
interface TelegramApiResponse {
|
|
284
354
|
ok: boolean
|
|
285
355
|
description?: string
|
|
286
|
-
|
|
356
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
357
|
+
result?: any
|
|
287
358
|
}
|
|
288
359
|
|
|
289
360
|
function formatTelegramName(from?: TelegramMessage['from']): string {
|
|
@@ -292,3 +363,7 @@ function formatTelegramName(from?: TelegramMessage['from']): string {
|
|
|
292
363
|
if (from.last_name) parts.push(from.last_name)
|
|
293
364
|
return parts.join(' ')
|
|
294
365
|
}
|
|
366
|
+
|
|
367
|
+
function sleep(ms: number): Promise<void> {
|
|
368
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
369
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto'
|
|
2
|
+
import { query } from '@anthropic-ai/claude-agent-sdk'
|
|
3
|
+
import { eq } from 'drizzle-orm'
|
|
4
|
+
import { getDb, schema } from '~~/server/db'
|
|
5
|
+
import { logTokenUsage } from '~~/server/utils/log-token-usage'
|
|
6
|
+
import { sendOutboundMessage } from './router'
|
|
7
|
+
import type { NormalizedMessage } from './types'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get or create the Main Chat conversation.
|
|
11
|
+
* There's exactly one conversation with isMain=true.
|
|
12
|
+
*/
|
|
13
|
+
async function getOrCreateMainChat() {
|
|
14
|
+
const db = getDb()
|
|
15
|
+
|
|
16
|
+
const [existing] = await db.select()
|
|
17
|
+
.from(schema.conversations)
|
|
18
|
+
.where(eq(schema.conversations.isMain, true))
|
|
19
|
+
.limit(1)
|
|
20
|
+
|
|
21
|
+
if (existing) return existing
|
|
22
|
+
|
|
23
|
+
const [created] = await db.insert(schema.conversations)
|
|
24
|
+
.values({
|
|
25
|
+
sessionId: `main-chat-${randomUUID()}`,
|
|
26
|
+
title: 'Main Chat',
|
|
27
|
+
isMain: true,
|
|
28
|
+
status: 'idle',
|
|
29
|
+
messageCount: 0,
|
|
30
|
+
totalCostUsd: 0
|
|
31
|
+
})
|
|
32
|
+
.returning()
|
|
33
|
+
|
|
34
|
+
console.log('[bridge] Created Main Chat conversation')
|
|
35
|
+
return created!
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// SDK result shape (same as agent-executor.ts)
|
|
39
|
+
interface SDKResult {
|
|
40
|
+
subtype: string
|
|
41
|
+
total_cost_usd: number
|
|
42
|
+
num_turns: number
|
|
43
|
+
duration_ms: number
|
|
44
|
+
result?: string
|
|
45
|
+
errors?: string[]
|
|
46
|
+
session_id?: string
|
|
47
|
+
usage: { input_tokens: number, output_tokens: number }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate a response to a bridge message using the Claude Agent SDK.
|
|
52
|
+
* Routes through the unified Main Chat conversation.
|
|
53
|
+
*/
|
|
54
|
+
export async function generateBridgeResponse(
|
|
55
|
+
bridgeId: string,
|
|
56
|
+
message: NormalizedMessage,
|
|
57
|
+
bridgeMessageId: string
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
const db = getDb()
|
|
60
|
+
const mainChat = await getOrCreateMainChat()
|
|
61
|
+
|
|
62
|
+
// Handle bridge commands
|
|
63
|
+
if (message.text.trim() === '/new') {
|
|
64
|
+
await db.update(schema.conversations)
|
|
65
|
+
.set({ sdkSessionId: null, summary: null })
|
|
66
|
+
.where(eq(schema.conversations.id, mainChat.id))
|
|
67
|
+
|
|
68
|
+
await sendOutboundMessage({
|
|
69
|
+
bridgeId,
|
|
70
|
+
platform: message.platform,
|
|
71
|
+
recipient: message.channelId || message.sender,
|
|
72
|
+
text: 'Conversation reset. Send me a message to start fresh.'
|
|
73
|
+
})
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Link inbound bridge message to main chat
|
|
78
|
+
await db.update(schema.bridgeMessages)
|
|
79
|
+
.set({ conversationId: mainChat.id })
|
|
80
|
+
.where(eq(schema.bridgeMessages.id, bridgeMessageId))
|
|
81
|
+
|
|
82
|
+
// Persist user message in the conversation
|
|
83
|
+
await db.insert(schema.conversationMessages).values({
|
|
84
|
+
conversationId: mainChat.id,
|
|
85
|
+
role: 'user',
|
|
86
|
+
content: JSON.stringify([{ type: 'text', text: message.text }]),
|
|
87
|
+
source: message.platform
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Build the prompt with platform context
|
|
91
|
+
const senderLabel = message.senderName || message.sender
|
|
92
|
+
const prompt = `[${message.platform} message from ${senderLabel}]: ${message.text}`
|
|
93
|
+
|
|
94
|
+
// Update status
|
|
95
|
+
await db.update(schema.conversations)
|
|
96
|
+
.set({ status: 'streaming' })
|
|
97
|
+
.where(eq(schema.conversations.id, mainChat.id))
|
|
98
|
+
|
|
99
|
+
let responseText = ''
|
|
100
|
+
let sdkSessionId: string | undefined
|
|
101
|
+
let costUsd = 0
|
|
102
|
+
let durationMs = 0
|
|
103
|
+
let inputTokens = 0
|
|
104
|
+
let outputTokens = 0
|
|
105
|
+
let numTurns = 0
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const result = await runQuery(prompt, mainChat.sdkSessionId || undefined)
|
|
109
|
+
responseText = result.text
|
|
110
|
+
sdkSessionId = result.sdkSessionId
|
|
111
|
+
costUsd = result.costUsd
|
|
112
|
+
durationMs = result.durationMs
|
|
113
|
+
inputTokens = result.inputTokens
|
|
114
|
+
outputTokens = result.outputTokens
|
|
115
|
+
numTurns = result.numTurns
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// If resume fails, retry without resume
|
|
118
|
+
if (mainChat.sdkSessionId) {
|
|
119
|
+
console.warn('[bridge] SDK resume failed, retrying fresh:', error instanceof Error ? error.message : error)
|
|
120
|
+
try {
|
|
121
|
+
const result = await runQuery(prompt, undefined)
|
|
122
|
+
responseText = result.text
|
|
123
|
+
sdkSessionId = result.sdkSessionId
|
|
124
|
+
costUsd = result.costUsd
|
|
125
|
+
durationMs = result.durationMs
|
|
126
|
+
inputTokens = result.inputTokens
|
|
127
|
+
outputTokens = result.outputTokens
|
|
128
|
+
numTurns = result.numTurns
|
|
129
|
+
} catch (retryError) {
|
|
130
|
+
console.error('[bridge] SDK query failed:', retryError)
|
|
131
|
+
responseText = 'Sorry, I ran into an issue processing your message. Please try again.'
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
console.error('[bridge] SDK query failed:', error)
|
|
135
|
+
responseText = 'Sorry, I ran into an issue processing your message. Please try again.'
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!responseText)
|
|
140
|
+
responseText = '(No response generated)'
|
|
141
|
+
|
|
142
|
+
// Update conversation metadata
|
|
143
|
+
const msgs = await db.select()
|
|
144
|
+
.from(schema.conversationMessages)
|
|
145
|
+
.where(eq(schema.conversationMessages.conversationId, mainChat.id))
|
|
146
|
+
|
|
147
|
+
await db.update(schema.conversations)
|
|
148
|
+
.set({
|
|
149
|
+
status: 'idle',
|
|
150
|
+
sdkSessionId: sdkSessionId || mainChat.sdkSessionId,
|
|
151
|
+
messageCount: msgs.length + 1, // +1 for the assistant message we're about to add
|
|
152
|
+
totalCostUsd: (mainChat.totalCostUsd || 0) + costUsd,
|
|
153
|
+
endedAt: new Date()
|
|
154
|
+
})
|
|
155
|
+
.where(eq(schema.conversations.id, mainChat.id))
|
|
156
|
+
|
|
157
|
+
// Persist assistant message
|
|
158
|
+
await db.insert(schema.conversationMessages).values({
|
|
159
|
+
conversationId: mainChat.id,
|
|
160
|
+
role: 'assistant',
|
|
161
|
+
content: JSON.stringify([{ type: 'text', text: responseText }]),
|
|
162
|
+
costUsd,
|
|
163
|
+
durationMs
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// Log token usage
|
|
167
|
+
if (costUsd > 0 || inputTokens > 0) {
|
|
168
|
+
logTokenUsage({
|
|
169
|
+
source: 'bridge',
|
|
170
|
+
sourceId: mainChat.id,
|
|
171
|
+
sourceName: `Bridge: ${message.platform}`,
|
|
172
|
+
inputTokens,
|
|
173
|
+
outputTokens,
|
|
174
|
+
costUsd,
|
|
175
|
+
durationMs,
|
|
176
|
+
numTurns
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Send response back through the bridge adapter
|
|
181
|
+
await sendOutboundMessage({
|
|
182
|
+
bridgeId,
|
|
183
|
+
platform: message.platform,
|
|
184
|
+
recipient: message.channelId || message.sender,
|
|
185
|
+
text: responseText
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Run a Claude Agent SDK query and extract the response.
|
|
191
|
+
*/
|
|
192
|
+
interface QueryResult {
|
|
193
|
+
text: string
|
|
194
|
+
sdkSessionId?: string
|
|
195
|
+
costUsd: number
|
|
196
|
+
durationMs: number
|
|
197
|
+
inputTokens: number
|
|
198
|
+
outputTokens: number
|
|
199
|
+
numTurns: number
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function runQuery(
|
|
203
|
+
prompt: string,
|
|
204
|
+
resumeSessionId?: string
|
|
205
|
+
): Promise<QueryResult> {
|
|
206
|
+
const projectDir = process.env.COGNOVA_PROJECT_DIR || process.cwd()
|
|
207
|
+
|
|
208
|
+
const conversation = query({
|
|
209
|
+
prompt,
|
|
210
|
+
options: {
|
|
211
|
+
cwd: projectDir,
|
|
212
|
+
settingSources: ['user', 'project'],
|
|
213
|
+
permissionMode: 'bypassPermissions',
|
|
214
|
+
allowDangerouslySkipPermissions: true,
|
|
215
|
+
maxTurns: 50,
|
|
216
|
+
...(resumeSessionId ? { resume: resumeSessionId } : {})
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
let text = ''
|
|
221
|
+
let sdkSessionId: string | undefined
|
|
222
|
+
let costUsd = 0
|
|
223
|
+
let durationMs = 0
|
|
224
|
+
let inputTokens = 0
|
|
225
|
+
let outputTokens = 0
|
|
226
|
+
let numTurns = 0
|
|
227
|
+
|
|
228
|
+
for await (const message of conversation) {
|
|
229
|
+
if (message.type === 'system' && (message as { subtype?: string }).subtype === 'init') {
|
|
230
|
+
sdkSessionId = (message as { session_id?: string }).session_id
|
|
231
|
+
} else if (message.type === 'result') {
|
|
232
|
+
const msg = message as unknown as SDKResult
|
|
233
|
+
if (msg.subtype === 'success' && msg.result)
|
|
234
|
+
text = msg.result
|
|
235
|
+
else if (msg.errors?.length)
|
|
236
|
+
text = msg.errors.join('\n')
|
|
237
|
+
|
|
238
|
+
sdkSessionId = msg.session_id || sdkSessionId
|
|
239
|
+
costUsd = msg.total_cost_usd || 0
|
|
240
|
+
durationMs = msg.duration_ms || 0
|
|
241
|
+
inputTokens = msg.usage?.input_tokens || 0
|
|
242
|
+
outputTokens = msg.usage?.output_tokens || 0
|
|
243
|
+
numTurns = msg.num_turns || 0
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { text, sdkSessionId, costUsd, durationMs, inputTokens, outputTokens, numTurns }
|
|
248
|
+
}
|
package/server/bridge/router.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { eq } from 'drizzle-orm'
|
|
2
2
|
import { getDb, schema } from '~~/server/db'
|
|
3
3
|
import { notifyResourceChange } from '~~/server/utils/notify-resource'
|
|
4
|
+
import { generateBridgeResponse } from './responder'
|
|
4
5
|
import { getAdapter } from './registry'
|
|
5
6
|
import type { NormalizedMessage, OutboundMessage, DeliveryResult } from './types'
|
|
6
7
|
|
|
@@ -38,10 +39,11 @@ export async function handleInboundMessage(
|
|
|
38
39
|
meta: { platform: message.platform, sender: message.sender, direction: 'inbound' }
|
|
39
40
|
})
|
|
40
41
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// Route to Claude agent for response (fire-and-forget)
|
|
43
|
+
console.log(`[bridge] Inbound from ${message.sender} via ${message.platform}: ${message.text.substring(0, 100)}`)
|
|
44
|
+
void generateBridgeResponse(bridgeId, message, stored!.id).catch((error) => {
|
|
45
|
+
console.error('[bridge] Failed to generate response:', error)
|
|
46
|
+
})
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
/**
|
package/server/db/schema.ts
CHANGED
|
@@ -131,6 +131,7 @@ export const conversations = pgTable('conversations', {
|
|
|
131
131
|
enum: ['idle', 'streaming', 'interrupted', 'error']
|
|
132
132
|
}).default('idle').notNull(),
|
|
133
133
|
totalCostUsd: real('total_cost_usd').default(0).notNull(),
|
|
134
|
+
isMain: boolean('is_main').default(false).notNull(),
|
|
134
135
|
startedAt: timestamp('started_at', { withTimezone: true }).defaultNow().notNull(),
|
|
135
136
|
endedAt: timestamp('ended_at', { withTimezone: true }),
|
|
136
137
|
messageCount: integer('message_count').default(0).notNull()
|
|
@@ -143,6 +144,7 @@ export const conversationMessages = pgTable('conversation_messages', {
|
|
|
143
144
|
.references(() => conversations.id, { onDelete: 'cascade' }),
|
|
144
145
|
role: text('role', { enum: ['user', 'assistant'] }).notNull(),
|
|
145
146
|
content: text('content').notNull(), // JSON string of ChatContentBlock[]
|
|
147
|
+
source: text('source', { enum: ['web', 'telegram', 'discord', 'imessage', 'email', 'google'] }),
|
|
146
148
|
costUsd: real('cost_usd'),
|
|
147
149
|
durationMs: integer('duration_ms'),
|
|
148
150
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull()
|
|
@@ -383,7 +385,7 @@ export const skillsCatalog = pgTable('skills_catalog', {
|
|
|
383
385
|
|
|
384
386
|
export const tokenUsage = pgTable('token_usage', {
|
|
385
387
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
386
|
-
source: text('source', { enum: ['chat', 'agent', 'memory_extraction'] }).notNull(),
|
|
388
|
+
source: text('source', { enum: ['chat', 'agent', 'memory_extraction', 'bridge'] }).notNull(),
|
|
387
389
|
sourceId: text('source_id'),
|
|
388
390
|
sourceName: text('source_name'),
|
|
389
391
|
inputTokens: integer('input_tokens').default(0).notNull(),
|