Single entry point for every inbound event from any adapter.
(t: Transport, m: Incoming)
| 78 | } |
| 79 | |
| 80 | private key(t: Transport, m: Incoming): string { return `${t.platform}:${m.chatId}`; } |
| 81 | |
| 82 | /** The small control surface a command may drive for this conversation. */ |
| 83 | private controlsFor(t: Transport, chatId: string, key: string): GatewayControls { |
| 84 | return { |
| 85 | isBusy: () => this.busy.has(key), |
| 86 | queueDepth: () => this.queue.get(key)?.length ?? 0, |
| 87 | abort: () => { |
| 88 | const ac = this.busy.get(key); |
| 89 | if (!ac) return false; |
| 90 | ac.abort(); |
| 91 | getOperatorHub().cancelLane(botLane(key)); |
| 92 | return true; |
| 93 | }, |
| 94 | reset: async () => { |
| 95 | this.busy.get(key)?.abort(); |
| 96 | this.queue.delete(key); |
| 97 | getOperatorHub().cancelLane(botLane(key)); |
| 98 | await this.opts.agent.reset?.(key); |
| 99 | }, |
| 100 | runTask: (txt) => this.runAndDrain(t, chatId, key, txt), |
| 101 | }; |
| 102 | } |
| 103 | |
| 104 | /** Run `text` as a turn, serialized per chat: if busy, queue it; else run + drain the queue. */ |
| 105 | private async runAndDrain(t: Transport, chatId: string, key: string, text: string): Promise<void> { |
| 106 | if (this.busy.has(key)) { |
| 107 | const q = this.queue.get(key) ?? []; |
| 108 | q.push(text); this.queue.set(key, q); |
| 109 | await t.send(chatId, `⏳ Busy — queued (#${q.length}). \`/stop\` to cancel the current task.`); |
| 110 | return; |
| 111 | } |
| 112 | await this.runOne(t, chatId, key, text); |
| 113 | let next: string | undefined; |
| 114 | while ((next = this.queue.get(key)?.shift())) await this.runOne(t, chatId, key, next); |
| 115 | this.queue.delete(key); |
| 116 | } |
| 117 | |
| 118 | /** Single entry point for every inbound event from any adapter. */ |
| 119 | async onMessage(t: Transport, m: Incoming): Promise<void> { |
| 120 | const key = this.key(t, m); |
| 121 | |
| 122 | // 1) Auth — closed by default. A coding agent must never run for an unlisted user. |
| 123 | if (!isAuthorized(t.platform, m.userId, this.opts.allow)) { |
| 124 | await t.send(m.chatId, '⛔ You are not on this QodeX bot’s allowlist.'); |
| 125 | return; |
| 126 | } |
| 127 | |
| 128 | // 2) Living-artifact card actions (Approve / Edit / Reject) — handled before permission asks. |
| 129 | if (m.callbackData?.startsWith('art:')) { |
| 130 | if (m.callbackId && t.ackCallback) await t.ackCallback(m.callbackId); |
| 131 | await this.handleCardAction(t, m.chatId, key, m.callbackData); |
no test coverage detected