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.
Files changed (199) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/{CH_jROT9.js → 0NJ3PaRM.js} +1 -1
  3. package/.output/public/_nuxt/{C075hYlX.js → 0yk-pS3R.js} +2 -2
  4. package/.output/public/_nuxt/{CVYcsqmL.js → 10_wwHSz.js} +1 -1
  5. package/.output/public/_nuxt/{BUC36BHp.js → 1zUTf4AP.js} +1 -1
  6. package/.output/public/_nuxt/{njnzCNrH.js → 2dNDtTiv.js} +3 -3
  7. package/.output/public/_nuxt/{B8TQvzYX.js → 2nOqGUPr.js} +1 -1
  8. package/.output/public/_nuxt/{ijmD2cEW.js → 3jQMk_5H.js} +1 -1
  9. package/.output/public/_nuxt/{D4X6ONPF.js → 4K03TkGA.js} +1 -1
  10. package/.output/public/_nuxt/{C3tximD1.js → 6boMs_nF.js} +1 -1
  11. package/.output/public/_nuxt/{Drxf7nJz.js → 7oCGSglN.js} +1 -1
  12. package/.output/public/_nuxt/{Dt5mOzXj.js → 8q5NepGW.js} +1 -1
  13. package/.output/public/_nuxt/{GYf-vnR6.js → B93pdGAW.js} +1 -1
  14. package/.output/public/_nuxt/{BZBNJD7U.js → BA_pRWwX.js} +1 -1
  15. package/.output/public/_nuxt/{eE8TtXgC.js → BDyn4ApB.js} +3 -3
  16. package/.output/public/_nuxt/{0dqQoCHY.js → BGgwYWjH.js} +1 -1
  17. package/.output/public/_nuxt/{BrOKokPO.js → BH0QDWEm.js} +1 -1
  18. package/.output/public/_nuxt/{B5SnZL2l.js → BHtY0l0l.js} +1 -1
  19. package/.output/public/_nuxt/{ImPsUDiH.js → BIXrSYwR.js} +1 -1
  20. package/.output/public/_nuxt/BK8S2ony.js +1 -0
  21. package/.output/public/_nuxt/{Bkc1Hpcf.js → BKXg-alD.js} +1 -1
  22. package/.output/public/_nuxt/{BahXyh-A.js → BLnYhy_t.js} +1 -1
  23. package/.output/public/_nuxt/{Bhhg7oSf.js → BR8bKz8h.js} +1 -1
  24. package/.output/public/_nuxt/{D2kQkOaG.js → BS2ZNXI1.js} +1 -1
  25. package/.output/public/_nuxt/{BLm0UviR.js → BUghTae1.js} +1 -1
  26. package/.output/public/_nuxt/{D4_89MZm.js → BXuWCWsJ.js} +1 -1
  27. package/.output/public/_nuxt/{LQNRVThr.js → BYHCP8x7.js} +1 -1
  28. package/.output/public/_nuxt/{BYtVdUIW.js → BZrSPX0S.js} +1 -1
  29. package/.output/public/_nuxt/{U3f6dy8i.js → B_wilgcd.js} +1 -1
  30. package/.output/public/_nuxt/BaZwjF8o.js +1 -0
  31. package/.output/public/_nuxt/{BefOhWHD.js → Bciqk4dX.js} +1 -1
  32. package/.output/public/_nuxt/{CewcVxDS.js → Beom-INj.js} +1 -1
  33. package/.output/public/_nuxt/{BlsIfvSf.js → BfcLZ_fC.js} +1 -1
  34. package/.output/public/_nuxt/{dB7G4iIC.js → Bk6JUtIo.js} +1 -1
  35. package/.output/public/_nuxt/BkfI94R8.js +1 -0
  36. package/.output/public/_nuxt/{DqzomZoo.js → BmOtR1wX.js} +1 -1
  37. package/.output/public/_nuxt/{4XnyUc7C.js → BmVLxnDU.js} +1 -1
  38. package/.output/public/_nuxt/{DtJnWtpL.js → BtHQ1T0f.js} +1 -1
  39. package/.output/public/_nuxt/{DcbbfwCe.js → ByYh0uRg.js} +1 -1
  40. package/.output/public/_nuxt/{CbvNJVrk.js → C27WoGNZ.js} +1 -1
  41. package/.output/public/_nuxt/{CY7f553E.js → C2FxZbO_.js} +1 -1
  42. package/.output/public/_nuxt/{zO9MgHj2.js → C2vq6Te8.js} +1 -1
  43. package/.output/public/_nuxt/{BXtZVbmx.js → C39dQ88F.js} +1 -1
  44. package/.output/public/_nuxt/{DmDPSvxi.js → C3NST0BU.js} +1 -1
  45. package/.output/public/_nuxt/{nHSiDk8N.js → C4mwL7SE.js} +1 -1
  46. package/.output/public/_nuxt/{BBDxhFYY.js → C5R5QaCA.js} +1 -1
  47. package/.output/public/_nuxt/{D79lbuWG.js → C6qKcgOY.js} +1 -1
  48. package/.output/public/_nuxt/{C-HCArYP.js → C71_a1IX.js} +1 -1
  49. package/.output/public/_nuxt/{CSjOaBKu.js → C9WIgRRL.js} +1 -1
  50. package/.output/public/_nuxt/{DmOS3vFH.js → CBTkrk2M.js} +1 -1
  51. package/.output/public/_nuxt/CEnSeCqn.js +1 -0
  52. package/.output/public/_nuxt/{CAfyXyeo.js → CKxSHyww.js} +1 -1
  53. package/.output/public/_nuxt/{D4Cizi4y.js → CLfF6dSn.js} +1 -1
  54. package/.output/public/_nuxt/{C7qhmmZh.js → CPutXj8l.js} +1 -1
  55. package/.output/public/_nuxt/{BCCYopTB.js → CQqCBrXj.js} +1 -1
  56. package/.output/public/_nuxt/{D-Ge96eV.js → CSKJ-Ahh.js} +1 -1
  57. package/.output/public/_nuxt/{B2Xfh7am.js → CTCcEJU3.js} +1 -1
  58. package/.output/public/_nuxt/{BnJ-6YDw.js → CVFwOzbl.js} +1 -1
  59. package/.output/public/_nuxt/{C21BjagS.js → CVdCqaby.js} +1 -1
  60. package/.output/public/_nuxt/{2QHXgMyi.js → CVqlefTY.js} +1 -1
  61. package/.output/public/_nuxt/CWyMCJQH.js +1 -0
  62. package/.output/public/_nuxt/{B6-U3KqJ.js → CY-QVcA5.js} +2 -2
  63. package/.output/public/_nuxt/{Cr0NfhPB.js → CY-cjAwJ.js} +1 -1
  64. package/.output/public/_nuxt/{Bwn-CRYH.js → CYPO5o_C.js} +1 -1
  65. package/.output/public/_nuxt/{B-AlyT2D.js → CZRnNmU8.js} +1 -1
  66. package/.output/public/_nuxt/{DwIYtFfA.js → CZVzFlpI.js} +1 -1
  67. package/.output/public/_nuxt/{BkO884Ds.js → CZoEPC_Q.js} +3 -3
  68. package/.output/public/_nuxt/{BJUff9Ku.js → Cc-2ziaZ.js} +1 -1
  69. package/.output/public/_nuxt/{BLwctszt.js → ChcO9s3o.js} +1 -1
  70. package/.output/public/_nuxt/{DX_9SfFb.js → Cl-LOSDV.js} +1 -1
  71. package/.output/public/_nuxt/{CSHcfejh.js → CobqYwkp.js} +1 -1
  72. package/.output/public/_nuxt/{B0xuHXvN.js → CqNwHCpo.js} +1 -1
  73. package/.output/public/_nuxt/{DMb_1D81.js → CuxrHsu-.js} +1 -1
  74. package/.output/public/_nuxt/{GGcn5KPW.js → CwPdIZ9J.js} +1 -1
  75. package/.output/public/_nuxt/{D7pKT3IW.js → D0ifH682.js} +1 -1
  76. package/.output/public/_nuxt/{KEeoKtq_.js → D3AZaldL.js} +1 -1
  77. package/.output/public/_nuxt/{BdxP21ep.js → D4UJwDQJ.js} +1 -1
  78. package/.output/public/_nuxt/{C-Ygk9x5.js → D5jZq8b3.js} +1 -1
  79. package/.output/public/_nuxt/{B6G3NLfW.js → D5q8SnqE.js} +1 -1
  80. package/.output/public/_nuxt/{CDkyQ1eQ.js → D8M722pn.js} +1 -1
  81. package/.output/public/_nuxt/{BjnQ1pI1.js → D8lwrAYS.js} +1 -1
  82. package/.output/public/_nuxt/{C13FKhqK.js → D9nmzBAw.js} +1 -1
  83. package/.output/public/_nuxt/{ClGn48iq.js → DC4idAGt.js} +1 -1
  84. package/.output/public/_nuxt/{vES4kgx7.js → DEd2xVbS.js} +1 -1
  85. package/.output/public/_nuxt/{CClsKJ9A.js → DH4YkDAi.js} +1 -1
  86. package/.output/public/_nuxt/{D-5A6cMB.js → DJTCT0bl.js} +1 -1
  87. package/.output/public/_nuxt/{D_O7SlHm.js → DNP5E1bC.js} +1 -1
  88. package/.output/public/_nuxt/{BF3tVKtX.js → DO9SFIh1.js} +1 -1
  89. package/.output/public/_nuxt/DPklr_tJ.js +2 -0
  90. package/.output/public/_nuxt/{mfAEx0Kz.js → DQM4eBdt.js} +1 -1
  91. package/.output/public/_nuxt/{cPPThz84.js → DQh6I9z9.js} +1 -1
  92. package/.output/public/_nuxt/{CXTrUfQ5.js → DS2wStH1.js} +1 -1
  93. package/.output/public/_nuxt/{CYQP4zpi.js → DTAStixR.js} +1 -1
  94. package/.output/public/_nuxt/{CEz288t8.js → DTGenhcA.js} +1 -1
  95. package/.output/public/_nuxt/{B9zgEO8N.js → DUAAXJ6Q.js} +1 -1
  96. package/.output/public/_nuxt/{CzAZpALW.js → DUVzIl3o.js} +1 -1
  97. package/.output/public/_nuxt/{DJt7Rogf.js → DXdwpJ-I.js} +1 -1
  98. package/.output/public/_nuxt/{MWUW1sIF.js → DY3uK7wM.js} +1 -1
  99. package/.output/public/_nuxt/{CfY_bsV0.js → DYbZBZet.js} +1 -1
  100. package/.output/public/_nuxt/{DZEuR9ms.js → D_4LPm1U.js} +1 -1
  101. package/.output/public/_nuxt/{CrmE-JGu.js → Db4KMnt8.js} +1 -1
  102. package/.output/public/_nuxt/{BwC62XRN.js → DcJbYLTp.js} +1 -1
  103. package/.output/public/_nuxt/{C6sX7TYf.js → DfF81NlA.js} +1 -1
  104. package/.output/public/_nuxt/{BHn8tRjg.js → DhI5cA_n.js} +1 -1
  105. package/.output/public/_nuxt/{CQ3tAemj.js → DhuOKJda.js} +1 -1
  106. package/.output/public/_nuxt/{D887s9k9.js → Di-Nc75e.js} +1 -1
  107. package/.output/public/_nuxt/{D_obX2Hj.js → Djs0Tlpa.js} +1 -1
  108. package/.output/public/_nuxt/DmmdPt_5.js +1 -0
  109. package/.output/public/_nuxt/{DKKY8zWm.js → Dn5a-guE.js} +1 -1
  110. package/.output/public/_nuxt/{BvstjGHC.js → DtjjnHnt.js} +1 -1
  111. package/.output/public/_nuxt/{Dbfwn2zk.js → DuvzM-P1.js} +1 -1
  112. package/.output/public/_nuxt/{BxShcW0K.js → DwY7rCd_.js} +1 -1
  113. package/.output/public/_nuxt/{BmaHzvZM.js → Dya5oK8u.js} +1 -1
  114. package/.output/public/_nuxt/{CO3hceDE.js → DzA58_Lm.js} +1 -1
  115. package/.output/public/_nuxt/{CF33v_m5.js → DzGy77Vr.js} +1 -1
  116. package/.output/public/_nuxt/{DmlZSBjF.js → E3rXPwU8.js} +1 -1
  117. package/.output/public/_nuxt/{CEDw8osP.js → EgKnQnf-.js} +1 -1
  118. package/.output/public/_nuxt/{it8syoZI.js → GtEM7xVU.js} +1 -1
  119. package/.output/public/_nuxt/{DLUvsixu.js → H6JbrRBU.js} +1 -1
  120. package/.output/public/_nuxt/{BE1CVWts.js → ILEvizzp.js} +1 -1
  121. package/.output/public/_nuxt/{BSA-U0so.js → JX1oqJI9.js} +1 -1
  122. package/.output/public/_nuxt/{yJ9vRvHj.js → JbHa4oXq.js} +1 -1
  123. package/.output/public/_nuxt/{zXvvpsZj.js → Kw0zy3FG.js} +1 -1
  124. package/.output/public/_nuxt/{C6FFW_cO.js → N5XtbYVD.js} +1 -1
  125. package/.output/public/_nuxt/{D04CyXmf.js → PP_4ebzl.js} +1 -1
  126. package/.output/public/_nuxt/{6QDVg5TO.js → SrncdpaW.js} +1 -1
  127. package/.output/public/_nuxt/{36ZHqDPo.js → U1MWjQMi.js} +1 -1
  128. package/.output/public/_nuxt/{mT7Mt2d7.js → Um1vPiAz.js} +1 -1
  129. package/.output/public/_nuxt/{CqFcWBkW.js → XCjS70z4.js} +1 -1
  130. package/.output/public/_nuxt/{C24so3m7.js → YX8avsvq.js} +2 -2
  131. package/.output/public/_nuxt/{DaBSQDif.js → _cy8R3nk.js} +1 -1
  132. package/.output/public/_nuxt/{D9aYkNhQ.js → apYB9dr5.js} +1 -1
  133. package/.output/public/_nuxt/builds/latest.json +1 -1
  134. package/.output/public/_nuxt/builds/meta/def74b99-d70c-4f30-aa29-70248cbeac7d.json +1 -0
  135. package/.output/public/_nuxt/entry.NKPfH2kE.css +1 -0
  136. package/.output/public/_nuxt/{BqTdsCnW.js → fbyIeNkc.js} +1 -1
  137. package/.output/public/_nuxt/{-2AbaFNH.js → g5MjDvm5.js} +1 -1
  138. package/.output/public/_nuxt/{C1tCdK-M.js → gTrVszwd.js} +1 -1
  139. package/.output/public/_nuxt/{BVEFmI4I.js → ixlNW2So.js} +1 -1
  140. package/.output/public/_nuxt/{DtyyytRx.js → nnQqD5pb.js} +1 -1
  141. package/.output/public/_nuxt/{pHCJ6lhw.js → rfGRTJJW.js} +1 -1
  142. package/.output/public/_nuxt/{Buf1PAiO.js → t8aDAkZ5.js} +1 -1
  143. package/.output/public/_nuxt/{BmdlymIW.js → vIOxcXKR.js} +1 -1
  144. package/.output/public/_nuxt/{Cve4psva.js → vScW1Zgm.js} +1 -1
  145. package/.output/public/_nuxt/{SfaNtyKS.js → wO6z2ugJ.js} +1 -1
  146. package/.output/public/_nuxt/{BhYoxmLV.js → x6FRJ5ac.js} +1 -1
  147. package/.output/public/_nuxt/{Pn7VD6uQ.js → zq-a1TeT.js} +1 -1
  148. package/.output/server/chunks/build/{chat-CZMiB68R.mjs → chat-CR3JIVEq.mjs} +65 -11
  149. package/.output/server/chunks/build/chat-CR3JIVEq.mjs.map +1 -0
  150. package/.output/server/chunks/build/client.precomputed.mjs +1 -1
  151. package/.output/server/chunks/build/server.mjs +3 -3
  152. package/.output/server/chunks/build/{settings-ByUXa46k.mjs → settings-B2KXoGcz.mjs} +213 -26
  153. package/.output/server/chunks/build/settings-B2KXoGcz.mjs.map +1 -0
  154. package/.output/server/chunks/build/styles.mjs +2 -2
  155. package/.output/server/chunks/build/{usage-CSrBh4Or.mjs → usage-BHdQZbfI.mjs} +9 -5
  156. package/.output/server/chunks/build/{usage-CSrBh4Or.mjs.map → usage-BHdQZbfI.mjs.map} +1 -1
  157. package/.output/server/chunks/nitro/nitro.mjs +1161 -891
  158. package/.output/server/chunks/nitro/nitro.mjs.map +1 -1
  159. package/.output/server/chunks/routes/_ws/chat.mjs +2 -1
  160. package/.output/server/chunks/routes/_ws/chat.mjs.map +1 -1
  161. package/.output/server/chunks/routes/api/conversations/_id_.delete.mjs +4 -1
  162. package/.output/server/chunks/routes/api/conversations/_id_.delete.mjs.map +1 -1
  163. package/.output/server/chunks/routes/api/index.get3.mjs +1 -1
  164. package/.output/server/chunks/routes/api/index.get3.mjs.map +1 -1
  165. package/.output/server/chunks/routes/api/usage/stats.get.mjs +2 -0
  166. package/.output/server/chunks/routes/api/usage/stats.get.mjs.map +1 -1
  167. package/.output/server/package.json +1 -1
  168. package/app/components/chat/ConversationList.vue +7 -1
  169. package/app/components/chat/MessageBubble.vue +45 -1
  170. package/app/components/usage/UsageRecordsTable.vue +4 -2
  171. package/app/components/usage/UsageSourceDonut.client.vue +4 -2
  172. package/app/components/usage/UsageTopConsumers.vue +4 -2
  173. package/app/composables/useChat.ts +3 -0
  174. package/app/pages/settings.vue +148 -9
  175. package/package.json +1 -1
  176. package/server/api/conversations/[id].delete.ts +9 -0
  177. package/server/api/conversations/index.get.ts +2 -1
  178. package/server/api/usage/stats.get.ts +2 -0
  179. package/server/bridge/adapters/telegram.ts +83 -8
  180. package/server/bridge/responder.ts +248 -0
  181. package/server/bridge/router.ts +6 -4
  182. package/server/db/schema.ts +3 -1
  183. package/server/drizzle/migrations/0015_great_mephistopheles.sql +2 -0
  184. package/server/drizzle/migrations/meta/0015_snapshot.json +1972 -0
  185. package/server/drizzle/migrations/meta/_journal.json +7 -0
  186. package/server/routes/_ws/chat.ts +2 -1
  187. package/server/utils/log-token-usage.ts +1 -1
  188. package/shared/types/index.ts +6 -0
  189. package/.output/public/_nuxt/BIo-9u_O.js +0 -1
  190. package/.output/public/_nuxt/BMs0mHEp.js +0 -1
  191. package/.output/public/_nuxt/BwyXWqBY.js +0 -1
  192. package/.output/public/_nuxt/CE9_ECxx.js +0 -1
  193. package/.output/public/_nuxt/DmcMCO8x.js +0 -1
  194. package/.output/public/_nuxt/JReYPlwF.js +0 -2
  195. package/.output/public/_nuxt/builds/meta/410e449d-9fe8-43a0-ae07-80797693b9b8.json +0 -1
  196. package/.output/public/_nuxt/entry._7ZkP07A.css +0 -1
  197. package/.output/public/_nuxt/iBaivqi8.js +0 -1
  198. package/.output/server/chunks/build/chat-CZMiB68R.mjs.map +0 -1
  199. 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 using the Bot API webhook mode.
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
- console.log('[telegram] No webhook URL configured using polling or manual webhook setup')
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(method: string, body?: Record<string, unknown>): Promise<TelegramApiResponse> {
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
- result?: Record<string, unknown>
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
+ }
@@ -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
- // TODO Phase 2+: Route to Claude agent for processing
42
- // This will create/resume a conversation and stream the agent response,
43
- // then send the response back through the originating adapter.
44
- console.log(`[bridge] Inbound message from ${message.sender} via ${message.platform}: ${message.text.substring(0, 100)}`)
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
  /**
@@ -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(),
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "conversation_messages" ADD COLUMN "source" text;--> statement-breakpoint
2
+ ALTER TABLE "conversations" ADD COLUMN "is_main" boolean DEFAULT false NOT NULL;