diff --git a/docs/guide.md b/docs/guide.md index d67a111..3ed6e01 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -275,8 +275,9 @@ count as a mention. question on both the terminal and Telegram at once and returning whichever the user answers first. Otherwise it stays mounted alongside `ask` whenever the bridge is running with a paired owner, so a locally injected turn (a scheduled - tick, an extension-composed prompt) can still reach Telegram: with no terminal - to ask at, the question goes to this session's topic, else the owner's DM. + tick, an extension-composed prompt) can still reach Telegram. Without a + pre-resolved chat, it falls back to this session's topic or the owner's DM and + also shows the terminal picker when one is available. Requests are responder-, chat-, topic-, message-, and nonce-bound, stay answerable while the owning session runs, and use the shared state directory for cross-process answers. diff --git a/src/index.ts b/src/index.ts index c8f9daa..842f906 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1986,11 +1986,11 @@ export default function telegramExtension(pi: ExtensionAPI): void { const questions: PromptQuestion[] = p.questions.map((q) => ({ ...q, options: q.options ?? [] })); const canTerminal = ctx?.hasUI === true && typeof ctx.ui?.askDialog === "function"; let resolved = activePromptTarget ? { ...activePromptTarget } : undefined; - if (!resolved && !canTerminal && token.length > 0) { - // Headless turn the bridge never resolved a destination for (a locally - // injected or scheduled prompt with notify off). Telegram is the only - // surface left, so fall back to the destination telegram_send already - // uses: this session's topic, else the paired owner's DM. + if (!resolved && token.length > 0) { + // A resumed or locally injected turn may not have pre-resolved a + // destination. Fall back to the same destination telegram_send uses + // so an explicit telegram_ask can reach Telegram alongside a terminal: + // this session's topic, else the paired owner's DM. const a = loadAccess(warn); const ownerId = pairedOwnerId(a); const own = ownTopic && a.topicsChat ? { chatId: a.topicsChat, threadId: ownTopic.threadId } : undefined; diff --git a/src/index.wiring.test.ts b/src/index.wiring.test.ts index 99de6b6..a10f0a0 100644 --- a/src/index.wiring.test.ts +++ b/src/index.wiring.test.ts @@ -619,6 +619,47 @@ describe("telegram_ask execute (dual-surface)", () => { ); }); + test("resumed daemon UI ask falls back to the paired owner", async () => { + writeAccess({ + enabled: true, + allowFrom: ["42"], + notifyChat: "42", + notifyMode: "always", + profile: "daemon", + }); + const h = harness(["ask", "read"]); + await startBridge(h); + const calls: { method: string; body: Record }[] = []; + const realFetch = globalThis.fetch; + globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => { + calls.push({ + method: String(input).split("/").pop()!, + body: JSON.parse(String(init?.body ?? "{}")), + }); + return new Response(JSON.stringify({ ok: true, result: { message_id: 7 } }), { + headers: { "content-type": "application/json" }, + }); + }) as typeof fetch; + try { + const res = await h.tools.get("telegram_ask")!.execute( + "t", + { questions }, + undefined, + undefined, + { + hasUI: true, + ui: { askDialog: (qs: DialogQuestion[]) => submit(qs) }, + }, + ); + expect(res.content[0].text).toBe( + 'Ask provenance: {"posted":["terminal","telegram"],"answeredBy":"terminal","errors":{}}\nUser selected: A', + ); + expect(calls[0]).toMatchObject({ method: "sendMessage", body: { chat_id: "42" } }); + } finally { + globalThis.fetch = realFetch; + } + }); + test("terminal answer waits for a deferred Telegram post before reporting provenance", async () => { const h = harness(["ask", "read"]); await activateDualSurfaces(h);