| 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); |
| 132 | return; |
| 133 | } |
| 134 | |
| 135 | // 3) A pending permission prompt swallows the next tap/reply (not a new turn). |
| 136 | if (this.asks.has(key)) { |
| 137 | const answer = m.callbackData ? this.stripAsk(m.callbackData) : m.text.trim(); |
| 138 | if (m.callbackId && t.ackCallback) await t.ackCallback(m.callbackId); |
| 139 | if (answer) { this.resolveAsk(key, answer); return; } |
| 140 | } |
| 141 | if (m.callbackData) { if (m.callbackId && t.ackCallback) await t.ackCallback(m.callbackId); return; } |
| 142 | |
| 143 | let text = m.text.trim(); |
| 144 | if (!text) return; |
| 145 | |
| 146 | // 4) An "Edit" tap left this chat awaiting the change instruction → fold it into an update turn. |
| 147 | const editId = this.pendingEdit.get(key); |
| 148 | if (editId && !text.startsWith('/')) { |
| 149 | this.pendingEdit.delete(key); |
| 150 | text = `Update the "${editId}" artifact and keep it live: ${text}`; |
| 151 | } |
| 152 | |
| 153 | // 5) Commands — dispatched through the declarative registry (drives the native `/` menu + /help). |
| 154 | if (text.startsWith('/')) { |
| 155 | const hit = findCommand(text); |
| 156 | if (!hit) { await t.send(m.chatId, `Unknown command \`${text.split(/\s/)[0]}\`. Try /help.`); return; } |
| 157 | await hit.cmd.run({ |
| 158 | args: hit.args, key, agent: this.opts.agent, |
| 159 | gateway: this.controlsFor(t, m.chatId, key), |
| 160 | reply: async (s, b) => { await t.send(m.chatId, s, b); }, |
| 161 | }); |
| 162 | return; |
| 163 | } |
| 164 | |
| 165 | // 6) Run it as a turn (serialized per chat; queued if one is already running). |
| 166 | await this.runAndDrain(t, m.chatId, key, text); |
| 167 | } |
| 168 | |
| 169 | private async runOne(t: Transport, chatId: string, key: string, text: string): Promise<void> { |
| 170 | const ac = new AbortController(); |
| 171 | this.busy.set(key, ac); |