Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 5 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
41 changes: 41 additions & 0 deletions src/index.wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> }[] = [];
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);
Expand Down
Loading