@llblab/pi-telegram 0.6.1 → 0.6.3
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.
- package/README.md +13 -7
- package/docs/architecture.md +19 -19
- package/docs/outbound-handlers.md +15 -10
- package/index.ts +176 -151
- package/lib/config.ts +4 -6
- package/lib/locks.ts +2 -10
- package/lib/media.ts +23 -12
- package/lib/outbound-handlers.ts +94 -26
- package/lib/polling.ts +1 -1
- package/lib/prompts.ts +16 -9
- package/lib/queue.ts +87 -4
- package/lib/routing.ts +1 -1
- package/lib/runtime.ts +0 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -135,11 +135,11 @@ If you ask pi for a file or generated artifact (e.g., _"generate a shell script
|
|
|
135
135
|
|
|
136
136
|
### Assistant-Authored Outbound Actions
|
|
137
137
|
|
|
138
|
-
Assistant replies can include hidden outbound blocks. `telegram_voice` and `telegram_button` are not pi tools; they are assistant-authored HTML comments that the bridge removes from Telegram text and handles after `agent_end`.
|
|
138
|
+
Assistant replies can include hidden outbound blocks. `telegram_voice` and `telegram_button` are not pi tools; they are assistant-authored HTML comments that the bridge removes from Telegram text and handles after `agent_end`. Recognized blocks must start at column zero on a top-level line outside fenced code, quotes, and lists, so documentation examples remain literal. The agent writes normal Markdown; the extension owns voice generation, button markup, callback routing, and delivery.
|
|
139
139
|
|
|
140
140
|
#### Voice
|
|
141
141
|
|
|
142
|
-
Voice blocks synthesize their
|
|
142
|
+
Voice blocks synthesize their text and upload it as a native Telegram `sendVoice` OGG/Opus message. Use body form for multiline text, `text="..."` for explicit one-line text with optional attributes, and the colon shorthand for a one-line voice with no attributes. The spoken text may be a concise companion summary, but it does not have to follow that format; write what you want spoken and keep it TTS-friendly:
|
|
143
143
|
|
|
144
144
|
```md
|
|
145
145
|
Full technical answer stays readable as text.
|
|
@@ -147,6 +147,10 @@ Full technical answer stays readable as text.
|
|
|
147
147
|
<!-- telegram_voice lang=ru rate=+30%
|
|
148
148
|
Text to synthesize as a Telegram voice message.
|
|
149
149
|
-->
|
|
150
|
+
|
|
151
|
+
<!-- telegram_voice lang=ru rate=+30% text="Short spoken companion summary." -->
|
|
152
|
+
|
|
153
|
+
<!-- telegram_voice: Short spoken companion summary. -->
|
|
150
154
|
```
|
|
151
155
|
|
|
152
156
|
Outbound voice is disabled unless a matching `outboundHandlers[]` entry is configured. Multiple `telegram_voice` blocks in one reply are synthesized and sent independently, preserving each block's attributes. The bridge uses the same [command-template contract](./docs/command-templates.md) as inbound attachment handlers: split the template into args, substitute placeholders, execute without a shell, and use stdout as the result channel for a single template.
|
|
@@ -171,19 +175,21 @@ A TTS plus MP3-to-OGG setup can be expressed as `template: [...]`. The bridge pr
|
|
|
171
175
|
|
|
172
176
|
#### Buttons
|
|
173
177
|
|
|
174
|
-
Button blocks attach inline quick replies to the final text. Use one independent `telegram_button` block per action
|
|
178
|
+
Button blocks attach inline quick replies to the final text. Use one independent `telegram_button` block per action. If the prompt should equal the label, use the colon shorthand. If the prompt differs, use the inline `prompt="..."` attribute for one-line prompts or the body form for multiline prompts:
|
|
175
179
|
|
|
176
180
|
```md
|
|
177
181
|
I can continue.
|
|
178
182
|
|
|
179
|
-
<!-- telegram_button label="Continue"
|
|
180
|
-
|
|
183
|
+
<!-- telegram_button label=Continue prompt="Continue with the current plan." -->
|
|
184
|
+
|
|
185
|
+
<!-- telegram_button label="Show risks"
|
|
186
|
+
List the main risks first.
|
|
181
187
|
-->
|
|
182
188
|
|
|
183
|
-
<!-- telegram_button
|
|
189
|
+
<!-- telegram_button: OK -->
|
|
184
190
|
```
|
|
185
191
|
|
|
186
|
-
Button prompts are routed back into the normal Telegram queue as prompt turns. Outbound handler details are documented in [`docs/outbound-handlers.md`](./docs/outbound-handlers.md).
|
|
192
|
+
Button prompts are routed back into the normal Telegram queue as prompt turns. Keep the opening comment unclosed until the body-ending `-->` for body-form buttons. Closed heads must use `prompt="..."` or the colon shorthand to create a button. Outbound handler details are documented in [`docs/outbound-handlers.md`](./docs/outbound-handlers.md).
|
|
187
193
|
|
|
188
194
|
## Streaming
|
|
189
195
|
|
package/docs/architecture.md
CHANGED
|
@@ -23,23 +23,23 @@ Naming rule: because the repository already scopes this codebase to Telegram, ex
|
|
|
23
23
|
|
|
24
24
|
Current runtime areas use these ownership boundaries:
|
|
25
25
|
|
|
26
|
-
| Domain
|
|
27
|
-
|
|
|
28
|
-
| `index.ts`
|
|
29
|
-
| `api`
|
|
30
|
-
| `config` / `setup`
|
|
31
|
-
| `locks` / `polling`
|
|
32
|
-
| `updates` / `routing`
|
|
33
|
-
| `media` / `turns` / `attachment-handlers` | Text/media extraction, media-group debounce, inbound downloads, turn building/editing, image reads, attachment-handler matching/execution/fallback output
|
|
34
|
-
| `queue`
|
|
35
|
-
| `runtime`
|
|
36
|
-
| `model` / `menu` / `commands`
|
|
37
|
-
| `preview` / `replies` / `rendering`
|
|
38
|
-
| `outbound-handlers`
|
|
39
|
-
| `attachments`
|
|
40
|
-
| `status`
|
|
41
|
-
| `lifecycle` / `prompts` / `pi`
|
|
42
|
-
| `command-templates`
|
|
26
|
+
| Domain | Owns |
|
|
27
|
+
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
28
|
+
| `index.ts` | Single composition root for live pi/Telegram ports, session state, API-bound transport adapters, and status updates |
|
|
29
|
+
| `api` | Bot API transport shapes/helpers, retries, file download, temp-dir lifecycle, inbound limits, chat actions, lazy bot-token clients, runtime error recording |
|
|
30
|
+
| `config` / `setup` | Persisted bot/session pairing state, authorization, first-user pairing, token prompting, env fallback, validation, config persistence |
|
|
31
|
+
| `locks` / `polling` | Singleton `locks.json` ownership, takeover/restart semantics, long-poll controller state, update offset persistence, poll-loop runtime wiring |
|
|
32
|
+
| `updates` / `routing` | Update classification/execution planning, paired authorization, reactions, edits, callbacks, and inbound route composition |
|
|
33
|
+
| `media` / `turns` / `attachment-handlers` | Text/media extraction, media-group debounce, inbound downloads, turn building/editing, image reads, attachment-handler matching/execution/fallback output |
|
|
34
|
+
| `queue` | Queue item contracts, lane admission/order, stores, mutations, dispatch readiness/runtime, prompt/control enqueueing, session and agent/tool lifecycle sequencing |
|
|
35
|
+
| `runtime` | Session-local coordination primitives: counters, lifecycle flags, setup guard, abort handler, typing-loop timers, prompt-dispatch flags, agent-end reset binding |
|
|
36
|
+
| `model` / `menu` / `commands` | Model identity/thinking levels, scoped model resolution, in-flight switching, inline status/model/thinking UI, slash commands, bot command registration |
|
|
37
|
+
| `preview` / `replies` / `rendering` | Preview lifecycle/transports, final reply delivery and reply parameters, Telegram HTML Markdown rendering, chunking, stable-preview snapshots |
|
|
38
|
+
| `outbound-handlers` | Assistant-authored outbound comments, generated reply artifacts, inline-keyboard callbacks, and post-`agent_end` outbound action delivery |
|
|
39
|
+
| `attachments` | `telegram_attach` registration, outbound attachment queueing, stat/limit checks, photo/document delivery classification |
|
|
40
|
+
| `status` | Status-bar/status-message rendering, queue-lane status views, redacted runtime event ring, grouped pi diagnostics |
|
|
41
|
+
| `lifecycle` / `prompts` / `pi` | pi hook registration, Telegram-specific before-agent prompt injection, centralized direct pi SDK imports and context adapters |
|
|
42
|
+
| `command-templates` | Portable shell-free command-template standard helpers, composition expansion, placeholder substitution, and executable resolution |
|
|
43
43
|
|
|
44
44
|
Boundary invariants:
|
|
45
45
|
|
|
@@ -112,7 +112,7 @@ Dispatch is gated by:
|
|
|
112
112
|
- `ctx.isIdle()` being true
|
|
113
113
|
- `ctx.hasPendingMessages()` being false
|
|
114
114
|
|
|
115
|
-
This prevents queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity. Telegram `/status` and `/model` execute immediately; the dispatch controller still serializes any deferred control items so a queued control action must settle before the next queued action can dispatch.
|
|
115
|
+
This prevents queue races around rapid follow-ups, `/compact`, and mixed local plus Telegram activity. Post-agent-end dispatch retries are scheduled through a session-bound deferred dispatcher that activates on session start, cancels timers on session shutdown, and skips callbacks from older generations before they touch `ExtensionContext`. Telegram `/status` and `/model` execute immediately; the dispatch controller still serializes any deferred control items so a queued control action must settle before the next queued action can dispatch.
|
|
116
116
|
|
|
117
117
|
### Abort Behavior
|
|
118
118
|
|
|
@@ -155,7 +155,7 @@ Telegram prompt responses use explicit delivery context to attach outbound text,
|
|
|
155
155
|
|
|
156
156
|
Outbound files are sent only after the active Telegram turn completes, must be staged through the `telegram_attach` tool, are staged atomically per tool call, are checked against a default 50 MiB limit configurable through `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES`, and use file-backed multipart blobs so large sends do not require preloading whole files into memory.
|
|
157
157
|
|
|
158
|
-
Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`. On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks specially before delivery; comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences. Voice maps to the first matching `outboundHandlers[]` entry with `type: "voice"`, synthesizes
|
|
158
|
+
Assistant-authored outbound actions use final-message markup instead of agent tool calls. Preview updates strip closed top-level HTML comments and currently open/partial top-level comment starts before rendering, so users do not see transient metadata even when streaming flushes happen after only `<`, `<!`, or `<!--`. On `agent_end`, the bridge removes top-level comments from the Markdown text reply, but treats column-zero top-level `<!-- telegram_voice ... -->` and `<!-- telegram_button ... -->` blocks specially before delivery; comments inside fenced code, quotes, lists, or indented examples stay literal, including fenced blocks with Markdown-valid indented closing fences. Voice maps to the first matching `outboundHandlers[]` entry with `type: "voice"`, synthesizes body text, `text="..."`, or colon shorthand through command-template execution, and uploads the generated OGG/Opus file via Telegram `sendVoice`; when no outbound voice handler is configured, it silently skips voice delivery. The `template: [...]` form can express TTS plus MP3-to-OGG conversion using configured templates and bridge-provided `{text}`, `{mp3}`, and `{ogg}` placeholders. Top-level `args` and `defaults` apply to all composed steps unless a step defines private values, top-level `timeout` wraps the whole sequence, and each step receives the previous step's stdout on stdin by default, without hard-coded filesystem defaults. Button blocks are built in: each `telegram_button` block becomes one inline-keyboard button on the final text, and callback clicks enqueue the configured prompt text as a normal Telegram prompt turn; the `telegram_button: Label` shorthand uses the same text for label and prompt, `prompt="..."` supports explicit one-line prompts, and body-form buttons use the body as the prompt. This keeps technical Markdown, code, tables, formulas, and numbered lists in the text channel when appropriate while allowing TTS-friendly voice messages and tappable continuations without invoking `telegram_attach` or extra transport tools.
|
|
159
159
|
|
|
160
160
|
## Interactive Controls
|
|
161
161
|
|
|
@@ -49,9 +49,13 @@ Full text answer stays here.
|
|
|
49
49
|
<!-- telegram_voice lang=ru rate=+30%
|
|
50
50
|
Text to synthesize as a Telegram voice message.
|
|
51
51
|
-->
|
|
52
|
+
|
|
53
|
+
<!-- telegram_voice lang=ru rate=+30% text="Short spoken companion summary." -->
|
|
54
|
+
|
|
55
|
+
<!-- telegram_voice: Short spoken companion summary. -->
|
|
52
56
|
```
|
|
53
57
|
|
|
54
|
-
The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice` block to `type: "voice"`, generates one file per block, and sends each file as an independent Telegram-native voice message. The opening `<!-- telegram_voice` marker must start at column zero on a top-level line outside fenced code, quotes, and lists; otherwise it is rendered as literal Markdown.
|
|
58
|
+
The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice` block to `type: "voice"`, generates one file per block, and sends each file as an independent Telegram-native voice message. The opening `<!-- telegram_voice` marker must start at column zero on a top-level line outside fenced code, quotes, and lists; otherwise it is rendered as literal Markdown. Body-form comments leave the opening line unclosed until the body-ending `-->`; closed heads can use `text="..."` for explicit one-line spoken text.
|
|
55
59
|
|
|
56
60
|
## Built-In Voice Placeholders
|
|
57
61
|
|
|
@@ -59,7 +63,7 @@ Voice outbound handlers receive these runtime placeholders:
|
|
|
59
63
|
|
|
60
64
|
| Placeholder | Value |
|
|
61
65
|
| ----------- | -------------------------------------------------------- |
|
|
62
|
-
| `{text}` | Voice
|
|
66
|
+
| `{text}` | Voice text from body, `text="..."`, or colon shorthand |
|
|
63
67
|
| `{lang}` | Optional markup override such as `lang=ru` |
|
|
64
68
|
| `{rate}` | Optional markup override such as `rate=+30%` |
|
|
65
69
|
| `{mp3}` | Flat temp artifact path under `~/.pi/agent/tmp/telegram` |
|
|
@@ -75,26 +79,27 @@ For one-step `template` handlers, stdout remains the default result channel: the
|
|
|
75
79
|
|
|
76
80
|
## Buttons Markup
|
|
77
81
|
|
|
78
|
-
Assistant replies can include independent button blocks. The
|
|
82
|
+
Assistant replies can include independent button blocks. The prompt is sent back to pi when the user taps the button; use the colon shorthand when the prompt should equal the label, `prompt="..."` for one-line prompts, or the body form for multiline prompts:
|
|
79
83
|
|
|
80
84
|
```md
|
|
81
85
|
I can continue.
|
|
82
86
|
|
|
83
|
-
<!-- telegram_button label="
|
|
84
|
-
Continue with the current plan.
|
|
85
|
-
-->
|
|
87
|
+
<!-- telegram_button label=Continue prompt="Continue with the current plan." -->
|
|
86
88
|
|
|
87
89
|
<!-- telegram_button label="Show risks"
|
|
88
90
|
List the main risks first.
|
|
89
91
|
-->
|
|
90
92
|
|
|
91
|
-
<!-- telegram_button
|
|
93
|
+
<!-- telegram_button: Done -->
|
|
92
94
|
```
|
|
93
95
|
|
|
94
96
|
Rules:
|
|
95
97
|
|
|
96
|
-
- `telegram_button
|
|
98
|
+
- `telegram_button: Label` creates one independent label-only button row whose prompt equals the label.
|
|
99
|
+
- `telegram_button label="Label" prompt="Prompt"` creates one independent button row whose prompt is the `prompt` attribute.
|
|
100
|
+
- `telegram_button label="Label"` with a body creates one independent button row whose prompt is the block body.
|
|
97
101
|
- The opening `<!-- telegram_button` marker must start at column zero on a top-level line outside fenced code, quotes, and lists; otherwise it is rendered as literal Markdown.
|
|
102
|
+
- Keep the canonical body form as `<!-- telegram_button label="Label"` + body + `-->`; closed heads must use `prompt="..."` or the colon shorthand to create a button.
|
|
98
103
|
- Use one block per button; this mirrors HTML's singular element model and avoids a nested button DSL inside comments.
|
|
99
104
|
- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
|
|
100
105
|
|
|
@@ -105,8 +110,8 @@ Buttons are built in and do not need a command template because they are pure Te
|
|
|
105
110
|
The extension injects Telegram-specific system prompt guidance so agents know the fast path:
|
|
106
111
|
|
|
107
112
|
- Write the full technical answer as normal Markdown.
|
|
108
|
-
- Add `telegram_voice` when a Telegram-native voice message is useful;
|
|
109
|
-
- Add `telegram_button label="..."` for
|
|
113
|
+
- Add `telegram_voice` when a Telegram-native voice message is useful; use body text, `text="..."`, or colon shorthand for the text to synthesize. A companion summary is optional, no specific summary format is required.
|
|
114
|
+
- Add `telegram_button: ...` when label equals prompt, `telegram_button label="..." prompt="..."` for one-line prompts, or `telegram_button label="..."` with a body for multiline prompts. If the reply contains only button/voice comment blocks, add a short visible marker (for example `Choose one:`) before them so Telegram always has a visible parent message for attachment.
|
|
110
115
|
- Do not call or register TTS/text-to-OGG/Telegram transport tools for voice or buttons; the bridge owns the configured outbound-handler pipeline and delivery.
|
|
111
116
|
|
|
112
117
|
This keeps the agent focused on semantics and lets the bridge handle low-latency Telegram adaptation.
|
package/index.ts
CHANGED
|
@@ -33,7 +33,10 @@ type RuntimeTelegramQueueItem = Queue.TelegramQueueItem<Pi.ExtensionContext>;
|
|
|
33
33
|
|
|
34
34
|
export default function (pi: Pi.ExtensionAPI) {
|
|
35
35
|
const piRuntime = Pi.createExtensionApiRuntimePorts(pi);
|
|
36
|
+
const { getThinkingLevel, sendUserMessage, setModel, setThinkingLevel } =
|
|
37
|
+
piRuntime;
|
|
36
38
|
const bridgeRuntime = Runtime.createTelegramBridgeRuntime();
|
|
39
|
+
const { abort, lifecycle, queue, setup, typing } = bridgeRuntime;
|
|
37
40
|
const configStore = Config.createTelegramConfigStore();
|
|
38
41
|
const lockRuntime = Locks.createTelegramLockRuntime<Pi.ExtensionContext>();
|
|
39
42
|
const activeTurnRuntime = Queue.createTelegramActiveTurnStore();
|
|
@@ -46,10 +49,21 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
46
49
|
const runtimeEvents = Status.createTelegramRuntimeEventRecorder({
|
|
47
50
|
getBotToken: configStore.getBotToken,
|
|
48
51
|
});
|
|
49
|
-
const
|
|
50
|
-
|
|
52
|
+
const recordRuntimeEvent = runtimeEvents.record;
|
|
53
|
+
const getContextModel = Pi.getExtensionContextModel;
|
|
54
|
+
const isIdle = Pi.isExtensionContextIdle;
|
|
55
|
+
const hasPendingMessages = Pi.hasExtensionContextPendingMessages;
|
|
56
|
+
const compact = Pi.compactExtensionContext;
|
|
57
|
+
const mediaGroupRuntime = Media.createTelegramMediaGroupController<
|
|
58
|
+
Api.TelegramMessage,
|
|
59
|
+
Pi.ExtensionContext
|
|
60
|
+
>();
|
|
51
61
|
const telegramQueueStore =
|
|
52
62
|
Queue.createTelegramQueueStore<Pi.ExtensionContext>();
|
|
63
|
+
const deferredQueueDispatchRuntime =
|
|
64
|
+
Queue.createTelegramDeferredQueueDispatchRuntime<Pi.ExtensionContext>({
|
|
65
|
+
recordRuntimeEvent,
|
|
66
|
+
});
|
|
53
67
|
const pollingControllerState = Polling.createTelegramPollingControllerState();
|
|
54
68
|
const { getStatusLines, updateStatus } =
|
|
55
69
|
Status.createTelegramBridgeStatusRuntime<
|
|
@@ -62,9 +76,9 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
62
76
|
),
|
|
63
77
|
getActiveSourceMessageIds: activeTurnRuntime.getSourceMessageIds,
|
|
64
78
|
hasActiveTurn: activeTurnRuntime.has,
|
|
65
|
-
hasDispatchPending:
|
|
66
|
-
isCompactionInProgress:
|
|
67
|
-
getActiveToolExecutions:
|
|
79
|
+
hasDispatchPending: lifecycle.hasDispatchPending,
|
|
80
|
+
isCompactionInProgress: lifecycle.isCompactionInProgress,
|
|
81
|
+
getActiveToolExecutions: lifecycle.getActiveToolExecutions,
|
|
68
82
|
hasPendingModelSwitch: pendingModelSwitchStore.has,
|
|
69
83
|
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
70
84
|
formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
|
|
@@ -75,25 +89,26 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
75
89
|
Pi.ExtensionContext,
|
|
76
90
|
ActivePiModel
|
|
77
91
|
>({
|
|
78
|
-
getContextModel
|
|
92
|
+
getContextModel,
|
|
79
93
|
updateStatus,
|
|
80
94
|
});
|
|
81
95
|
const queueMutationRuntime =
|
|
82
96
|
Queue.createTelegramQueueMutationController<Pi.ExtensionContext>({
|
|
83
97
|
...telegramQueueStore,
|
|
84
|
-
getNextPriorityReactionOrder:
|
|
85
|
-
bridgeRuntime.queue.getNextPriorityReactionOrder,
|
|
98
|
+
getNextPriorityReactionOrder: queue.getNextPriorityReactionOrder,
|
|
86
99
|
incrementNextPriorityReactionOrder:
|
|
87
|
-
|
|
100
|
+
queue.incrementNextPriorityReactionOrder,
|
|
88
101
|
updateStatus,
|
|
89
102
|
});
|
|
90
103
|
const attachmentHandlerRuntime =
|
|
91
|
-
AttachmentHandlers.createTelegramAttachmentHandlerRuntime<Pi.ExtensionContext>(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
AttachmentHandlers.createTelegramAttachmentHandlerRuntime<Pi.ExtensionContext>(
|
|
105
|
+
{
|
|
106
|
+
getHandlers: configStore.getAttachmentHandlers,
|
|
107
|
+
execCommand: CommandTemplates.execCommandTemplate,
|
|
108
|
+
getCwd: Pi.getExtensionContextCwd,
|
|
109
|
+
recordRuntimeEvent,
|
|
110
|
+
},
|
|
111
|
+
);
|
|
97
112
|
|
|
98
113
|
// --- Telegram API ---
|
|
99
114
|
|
|
@@ -111,19 +126,19 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
111
126
|
prepareTempDir,
|
|
112
127
|
} = Api.createDefaultTelegramBridgeApiRuntime({
|
|
113
128
|
getBotToken: configStore.getBotToken,
|
|
114
|
-
recordRuntimeEvent
|
|
129
|
+
recordRuntimeEvent,
|
|
115
130
|
});
|
|
116
131
|
|
|
117
132
|
// --- Message Delivery & Preview ---
|
|
118
133
|
|
|
119
134
|
const promptDispatchRuntime =
|
|
120
135
|
Runtime.createTelegramPromptDispatchRuntime<Pi.ExtensionContext>({
|
|
121
|
-
lifecycle
|
|
122
|
-
typing
|
|
136
|
+
lifecycle,
|
|
137
|
+
typing,
|
|
123
138
|
getDefaultChatId: activeTurnRuntime.getChatId,
|
|
124
139
|
sendTypingAction,
|
|
125
140
|
updateStatus,
|
|
126
|
-
recordRuntimeEvent
|
|
141
|
+
recordRuntimeEvent,
|
|
127
142
|
});
|
|
128
143
|
|
|
129
144
|
// --- Reply Runtime Wiring ---
|
|
@@ -144,16 +159,17 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
144
159
|
const dispatchNextQueuedTelegramTurn =
|
|
145
160
|
Queue.createTelegramQueueDispatchRuntime<Pi.ExtensionContext>({
|
|
146
161
|
...telegramQueueStore,
|
|
147
|
-
isCompactionInProgress:
|
|
162
|
+
isCompactionInProgress: lifecycle.isCompactionInProgress,
|
|
148
163
|
hasActiveTurn: activeTurnRuntime.has,
|
|
149
|
-
hasDispatchPending:
|
|
150
|
-
isIdle
|
|
151
|
-
hasPendingMessages
|
|
164
|
+
hasDispatchPending: lifecycle.hasDispatchPending,
|
|
165
|
+
isIdle,
|
|
166
|
+
hasPendingMessages,
|
|
167
|
+
hasDispatchContext: deferredQueueDispatchRuntime.isBound,
|
|
152
168
|
updateStatus,
|
|
153
169
|
sendTextReply,
|
|
154
|
-
recordRuntimeEvent
|
|
170
|
+
recordRuntimeEvent,
|
|
155
171
|
...promptDispatchRuntime,
|
|
156
|
-
sendUserMessage
|
|
172
|
+
sendUserMessage,
|
|
157
173
|
}).dispatchNext;
|
|
158
174
|
const previewRuntime = Preview.createTelegramAssistantPreviewRuntime({
|
|
159
175
|
getActiveTurn: activeTurnRuntime.get,
|
|
@@ -173,15 +189,15 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
173
189
|
Pi.ExtensionContext,
|
|
174
190
|
Model.ScopedTelegramModel<ActivePiModel>
|
|
175
191
|
>({
|
|
176
|
-
isIdle
|
|
192
|
+
isIdle,
|
|
177
193
|
getPendingModelSwitch: pendingModelSwitchStore.get,
|
|
178
194
|
setPendingModelSwitch: pendingModelSwitchStore.set,
|
|
179
195
|
getActiveTurn: activeTurnRuntime.get,
|
|
180
|
-
getAbortHandler:
|
|
181
|
-
hasAbortHandler:
|
|
182
|
-
getActiveToolExecutions:
|
|
183
|
-
allocateItemOrder:
|
|
184
|
-
allocateControlOrder:
|
|
196
|
+
getAbortHandler: abort.getHandler,
|
|
197
|
+
hasAbortHandler: abort.hasHandler,
|
|
198
|
+
getActiveToolExecutions: lifecycle.getActiveToolExecutions,
|
|
199
|
+
allocateItemOrder: queue.allocateItemOrder,
|
|
200
|
+
allocateControlOrder: queue.allocateControlOrder,
|
|
185
201
|
appendQueuedItem: queueMutationRuntime.append,
|
|
186
202
|
updateStatus,
|
|
187
203
|
});
|
|
@@ -192,12 +208,12 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
192
208
|
runtime: modelMenuRuntime,
|
|
193
209
|
createSettingsManager: Pi.createSettingsManager,
|
|
194
210
|
getActiveModel: currentModelRuntime.get,
|
|
195
|
-
getThinkingLevel
|
|
211
|
+
getThinkingLevel,
|
|
196
212
|
buildStatusHtml: Status.createTelegramStatusHtmlBuilder({
|
|
197
213
|
getActiveModel: currentModelRuntime.get,
|
|
198
214
|
}),
|
|
199
215
|
storeModelMenuState: modelMenuRuntime.storeState,
|
|
200
|
-
isIdle
|
|
216
|
+
isIdle,
|
|
201
217
|
canOfferInFlightModelSwitch: modelSwitchController.canOfferInFlightSwitch,
|
|
202
218
|
sendTextReply,
|
|
203
219
|
editInteractiveMessage,
|
|
@@ -206,6 +222,39 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
206
222
|
|
|
207
223
|
// --- Polling ---
|
|
208
224
|
|
|
225
|
+
const inboundRouteRuntime = Routing.createTelegramInboundRouteRuntime<
|
|
226
|
+
Api.TelegramUpdate,
|
|
227
|
+
Api.TelegramMessage,
|
|
228
|
+
Api.TelegramCallbackQuery,
|
|
229
|
+
Pi.ExtensionContext,
|
|
230
|
+
ActivePiModel
|
|
231
|
+
>({
|
|
232
|
+
configStore,
|
|
233
|
+
bridgeRuntime,
|
|
234
|
+
activeTurnRuntime,
|
|
235
|
+
mediaGroupRuntime,
|
|
236
|
+
telegramQueueStore,
|
|
237
|
+
queueMutationRuntime,
|
|
238
|
+
modelMenuRuntime,
|
|
239
|
+
currentModelRuntime,
|
|
240
|
+
modelSwitchController,
|
|
241
|
+
menuActions,
|
|
242
|
+
buttonActionStore,
|
|
243
|
+
attachmentHandlerRuntime,
|
|
244
|
+
updateStatus,
|
|
245
|
+
dispatchNextQueuedTelegramTurn,
|
|
246
|
+
answerCallbackQuery,
|
|
247
|
+
sendTextReply,
|
|
248
|
+
setMyCommands,
|
|
249
|
+
downloadFile: downloadTelegramBridgeFile,
|
|
250
|
+
getThinkingLevel,
|
|
251
|
+
setThinkingLevel,
|
|
252
|
+
setModel,
|
|
253
|
+
isIdle,
|
|
254
|
+
hasPendingMessages,
|
|
255
|
+
compact,
|
|
256
|
+
recordRuntimeEvent,
|
|
257
|
+
});
|
|
209
258
|
const pollingRuntime = Polling.createTelegramPollingControllerRuntime<
|
|
210
259
|
Api.TelegramUpdate,
|
|
211
260
|
Pi.ExtensionContext
|
|
@@ -216,42 +265,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
216
265
|
deleteWebhook,
|
|
217
266
|
getUpdates,
|
|
218
267
|
persistConfig: configStore.persist,
|
|
219
|
-
handleUpdate:
|
|
220
|
-
|
|
221
|
-
Api.TelegramMessage,
|
|
222
|
-
Api.TelegramCallbackQuery,
|
|
223
|
-
Pi.ExtensionContext,
|
|
224
|
-
ActivePiModel
|
|
225
|
-
>({
|
|
226
|
-
configStore,
|
|
227
|
-
bridgeRuntime,
|
|
228
|
-
activeTurnRuntime,
|
|
229
|
-
mediaGroupRuntime,
|
|
230
|
-
telegramQueueStore,
|
|
231
|
-
queueMutationRuntime,
|
|
232
|
-
modelMenuRuntime,
|
|
233
|
-
currentModelRuntime,
|
|
234
|
-
modelSwitchController,
|
|
235
|
-
menuActions,
|
|
236
|
-
buttonActionStore,
|
|
237
|
-
attachmentHandlerRuntime,
|
|
238
|
-
updateStatus,
|
|
239
|
-
dispatchNextQueuedTelegramTurn,
|
|
240
|
-
answerCallbackQuery,
|
|
241
|
-
sendTextReply,
|
|
242
|
-
setMyCommands,
|
|
243
|
-
downloadFile: downloadTelegramBridgeFile,
|
|
244
|
-
getThinkingLevel: piRuntime.getThinkingLevel,
|
|
245
|
-
setThinkingLevel: piRuntime.setThinkingLevel,
|
|
246
|
-
setModel: piRuntime.setModel,
|
|
247
|
-
isIdle: Pi.isExtensionContextIdle,
|
|
248
|
-
hasPendingMessages: Pi.hasExtensionContextPendingMessages,
|
|
249
|
-
compact: Pi.compactExtensionContext,
|
|
250
|
-
recordRuntimeEvent: runtimeEvents.record,
|
|
251
|
-
}).handleUpdate,
|
|
252
|
-
stopTypingLoop: bridgeRuntime.typing.stop,
|
|
268
|
+
handleUpdate: inboundRouteRuntime.handleUpdate,
|
|
269
|
+
stopTypingLoop: typing.stop,
|
|
253
270
|
updateStatus,
|
|
254
|
-
recordRuntimeEvent
|
|
271
|
+
recordRuntimeEvent,
|
|
255
272
|
});
|
|
256
273
|
const lockedPollingRuntime = Locks.createTelegramLockedPollingRuntime({
|
|
257
274
|
lock: lockRuntime,
|
|
@@ -259,32 +276,35 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
259
276
|
startPolling: pollingRuntime.start,
|
|
260
277
|
stopPolling: pollingRuntime.stop,
|
|
261
278
|
updateStatus,
|
|
262
|
-
recordRuntimeEvent
|
|
279
|
+
recordRuntimeEvent,
|
|
280
|
+
});
|
|
281
|
+
const queueSessionLifecycle = Queue.createTelegramSessionLifecycleRuntime<
|
|
282
|
+
Pi.ExtensionContext,
|
|
283
|
+
RuntimeTelegramQueueItem,
|
|
284
|
+
ActivePiModel
|
|
285
|
+
>({
|
|
286
|
+
getCurrentModel: getContextModel,
|
|
287
|
+
loadConfig: configStore.load,
|
|
288
|
+
setQueuedItems: telegramQueueStore.setQueuedItems,
|
|
289
|
+
setCurrentModel: currentModelRuntime.set,
|
|
290
|
+
setPendingModelSwitch: pendingModelSwitchStore.set,
|
|
291
|
+
syncCounters: queue.syncCounters,
|
|
292
|
+
syncFlags: lifecycle.syncFlags,
|
|
293
|
+
bindDeferredDispatchContext: deferredQueueDispatchRuntime.bind,
|
|
294
|
+
prepareTempDir,
|
|
295
|
+
updateStatus,
|
|
296
|
+
unbindDeferredDispatchContext: deferredQueueDispatchRuntime.unbind,
|
|
297
|
+
clearPendingMediaGroups: mediaGroupRuntime.clear,
|
|
298
|
+
clearModelMenuState: modelMenuRuntime.clear,
|
|
299
|
+
getActiveTurnChatId: activeTurnRuntime.getChatId,
|
|
300
|
+
clearPreview: previewRuntime.clear,
|
|
301
|
+
clearActiveTurn: activeTurnRuntime.clear,
|
|
302
|
+
clearAbort: abort.clearHandler,
|
|
303
|
+
stopPolling: lockedPollingRuntime.suspend,
|
|
304
|
+
recordRuntimeEvent,
|
|
263
305
|
});
|
|
264
306
|
const sessionLifecycleRuntime = Lifecycle.appendTelegramLifecycleHooks(
|
|
265
|
-
|
|
266
|
-
Pi.ExtensionContext,
|
|
267
|
-
RuntimeTelegramQueueItem,
|
|
268
|
-
ActivePiModel
|
|
269
|
-
>({
|
|
270
|
-
getCurrentModel: Pi.getExtensionContextModel,
|
|
271
|
-
loadConfig: configStore.load,
|
|
272
|
-
setQueuedItems: telegramQueueStore.setQueuedItems,
|
|
273
|
-
setCurrentModel: currentModelRuntime.set,
|
|
274
|
-
setPendingModelSwitch: pendingModelSwitchStore.set,
|
|
275
|
-
syncCounters: bridgeRuntime.queue.syncCounters,
|
|
276
|
-
syncFlags: bridgeRuntime.lifecycle.syncFlags,
|
|
277
|
-
prepareTempDir,
|
|
278
|
-
updateStatus,
|
|
279
|
-
clearPendingMediaGroups: mediaGroupRuntime.clear,
|
|
280
|
-
clearModelMenuState: modelMenuRuntime.clear,
|
|
281
|
-
getActiveTurnChatId: activeTurnRuntime.getChatId,
|
|
282
|
-
clearPreview: previewRuntime.clear,
|
|
283
|
-
clearActiveTurn: activeTurnRuntime.clear,
|
|
284
|
-
clearAbort: bridgeRuntime.abort.clearHandler,
|
|
285
|
-
stopPolling: lockedPollingRuntime.suspend,
|
|
286
|
-
recordRuntimeEvent: runtimeEvents.record,
|
|
287
|
-
}),
|
|
307
|
+
queueSessionLifecycle,
|
|
288
308
|
{ onSessionStart: lockedPollingRuntime.onSessionStart },
|
|
289
309
|
);
|
|
290
310
|
|
|
@@ -292,19 +312,19 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
292
312
|
|
|
293
313
|
Attachments.registerTelegramAttachmentTool(pi, {
|
|
294
314
|
getActiveTurn: activeTurnRuntime.get,
|
|
295
|
-
recordRuntimeEvent
|
|
315
|
+
recordRuntimeEvent,
|
|
296
316
|
});
|
|
297
317
|
|
|
298
318
|
Commands.registerTelegramBridgeCommands(pi, {
|
|
299
319
|
promptForConfig: Setup.createTelegramSetupPromptRuntime({
|
|
300
320
|
getConfig: configStore.get,
|
|
301
321
|
setConfig: configStore.set,
|
|
302
|
-
setupGuard:
|
|
322
|
+
setupGuard: setup,
|
|
303
323
|
getMe: Api.fetchTelegramBotIdentity,
|
|
304
324
|
persistConfig: configStore.persist,
|
|
305
325
|
startPolling: lockedPollingRuntime.start,
|
|
306
326
|
updateStatus,
|
|
307
|
-
recordRuntimeEvent
|
|
327
|
+
recordRuntimeEvent,
|
|
308
328
|
}),
|
|
309
329
|
getStatusLines,
|
|
310
330
|
reloadConfig: configStore.load,
|
|
@@ -316,66 +336,71 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
316
336
|
|
|
317
337
|
// --- Lifecycle Hooks ---
|
|
318
338
|
|
|
339
|
+
const agentEndResetter = Runtime.createTelegramAgentEndResetter({
|
|
340
|
+
abort,
|
|
341
|
+
typing,
|
|
342
|
+
clearActiveTurn: activeTurnRuntime.clear,
|
|
343
|
+
resetToolExecutions: lifecycle.resetActiveToolExecutions,
|
|
344
|
+
clearPendingModelSwitch: modelSwitchController.clearPendingSwitch,
|
|
345
|
+
clearDispatchPending: lifecycle.clearDispatchPending,
|
|
346
|
+
});
|
|
347
|
+
const queuedAttachmentSender =
|
|
348
|
+
Attachments.createTelegramQueuedAttachmentSender({
|
|
349
|
+
sendMultipart: callMultipart,
|
|
350
|
+
sendTextReply,
|
|
351
|
+
recordRuntimeEvent,
|
|
352
|
+
});
|
|
353
|
+
const outboundReplyPlanner =
|
|
354
|
+
OutboundHandlers.createTelegramOutboundReplyPlanner(buttonActionStore);
|
|
355
|
+
const outboundReplyArtifactSender =
|
|
356
|
+
OutboundHandlers.createTelegramOutboundReplyArtifactSender({
|
|
357
|
+
execCommand: CommandTemplates.execCommandTemplate,
|
|
358
|
+
sendMultipart: callMultipart,
|
|
359
|
+
sendTextReply,
|
|
360
|
+
getHandlers: configStore.getOutboundHandlers,
|
|
361
|
+
recordRuntimeEvent,
|
|
362
|
+
});
|
|
363
|
+
const agentLifecycleHooks = Queue.createTelegramAgentLifecycleHooks<
|
|
364
|
+
Queue.PendingTelegramTurn,
|
|
365
|
+
Pi.ExtensionContext,
|
|
366
|
+
unknown
|
|
367
|
+
>({
|
|
368
|
+
setAbortHandler: Runtime.createTelegramContextAbortHandlerSetter(abort),
|
|
369
|
+
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
370
|
+
hasPendingDispatch: lifecycle.hasDispatchPending,
|
|
371
|
+
hasActiveTurn: activeTurnRuntime.has,
|
|
372
|
+
resetToolExecutions: lifecycle.resetActiveToolExecutions,
|
|
373
|
+
resetPendingModelSwitch: modelSwitchController.clearPendingSwitch,
|
|
374
|
+
setQueuedItems: telegramQueueStore.setQueuedItems,
|
|
375
|
+
clearDispatchPending: lifecycle.clearDispatchPending,
|
|
376
|
+
setActiveTurn: activeTurnRuntime.set,
|
|
377
|
+
createPreviewState: previewRuntime.resetState,
|
|
378
|
+
startTypingLoop: promptDispatchRuntime.startTypingLoop,
|
|
379
|
+
updateStatus,
|
|
380
|
+
getActiveTurn: activeTurnRuntime.get,
|
|
381
|
+
extractAssistant: Replies.extractLatestAssistantMessageText,
|
|
382
|
+
getPreserveQueuedTurnsAsHistory: lifecycle.shouldPreserveQueuedTurnsAsHistory,
|
|
383
|
+
resetRuntimeState: agentEndResetter,
|
|
384
|
+
dispatchNextQueuedTelegramTurn,
|
|
385
|
+
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
386
|
+
deferredQueueDispatchRuntime.request,
|
|
387
|
+
clearPreview: previewRuntime.clear,
|
|
388
|
+
setPreviewPendingText: previewRuntime.setPendingText,
|
|
389
|
+
finalizeMarkdownPreview: previewRuntime.finalizeMarkdown,
|
|
390
|
+
sendMarkdownReply,
|
|
391
|
+
sendTextReply,
|
|
392
|
+
sendQueuedAttachments: queuedAttachmentSender,
|
|
393
|
+
planOutboundReply: outboundReplyPlanner,
|
|
394
|
+
sendOutboundReplyArtifacts: outboundReplyArtifactSender,
|
|
395
|
+
getActiveToolExecutions: lifecycle.getActiveToolExecutions,
|
|
396
|
+
setActiveToolExecutions: lifecycle.setActiveToolExecutions,
|
|
397
|
+
triggerPendingModelSwitchAbort: modelSwitchController.triggerPendingAbort,
|
|
398
|
+
});
|
|
319
399
|
Lifecycle.registerTelegramLifecycleHooks(pi, {
|
|
320
400
|
...sessionLifecycleRuntime,
|
|
401
|
+
...agentLifecycleHooks,
|
|
321
402
|
onBeforeAgentStart: Prompts.createTelegramBeforeAgentStartHook(),
|
|
322
403
|
onModelSelect: currentModelRuntime.onModelSelect,
|
|
323
|
-
...Queue.createTelegramAgentLifecycleHooks<
|
|
324
|
-
Queue.PendingTelegramTurn,
|
|
325
|
-
Pi.ExtensionContext,
|
|
326
|
-
unknown
|
|
327
|
-
>({
|
|
328
|
-
setAbortHandler: Runtime.createTelegramContextAbortHandlerSetter(
|
|
329
|
-
bridgeRuntime.abort,
|
|
330
|
-
),
|
|
331
|
-
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
332
|
-
hasPendingDispatch: bridgeRuntime.lifecycle.hasDispatchPending,
|
|
333
|
-
hasActiveTurn: activeTurnRuntime.has,
|
|
334
|
-
resetToolExecutions: bridgeRuntime.lifecycle.resetActiveToolExecutions,
|
|
335
|
-
resetPendingModelSwitch: modelSwitchController.clearPendingSwitch,
|
|
336
|
-
setQueuedItems: telegramQueueStore.setQueuedItems,
|
|
337
|
-
clearDispatchPending: bridgeRuntime.lifecycle.clearDispatchPending,
|
|
338
|
-
setActiveTurn: activeTurnRuntime.set,
|
|
339
|
-
createPreviewState: previewRuntime.resetState,
|
|
340
|
-
startTypingLoop: promptDispatchRuntime.startTypingLoop,
|
|
341
|
-
updateStatus,
|
|
342
|
-
getActiveTurn: activeTurnRuntime.get,
|
|
343
|
-
extractAssistant: Replies.extractLatestAssistantMessageText,
|
|
344
|
-
getPreserveQueuedTurnsAsHistory:
|
|
345
|
-
bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory,
|
|
346
|
-
resetRuntimeState: Runtime.createTelegramAgentEndResetter({
|
|
347
|
-
abort: bridgeRuntime.abort,
|
|
348
|
-
typing: bridgeRuntime.typing,
|
|
349
|
-
clearActiveTurn: activeTurnRuntime.clear,
|
|
350
|
-
resetToolExecutions: bridgeRuntime.lifecycle.resetActiveToolExecutions,
|
|
351
|
-
clearPendingModelSwitch: modelSwitchController.clearPendingSwitch,
|
|
352
|
-
clearDispatchPending: bridgeRuntime.lifecycle.clearDispatchPending,
|
|
353
|
-
}),
|
|
354
|
-
dispatchNextQueuedTelegramTurn,
|
|
355
|
-
clearPreview: previewRuntime.clear,
|
|
356
|
-
setPreviewPendingText: previewRuntime.setPendingText,
|
|
357
|
-
finalizeMarkdownPreview: previewRuntime.finalizeMarkdown,
|
|
358
|
-
sendMarkdownReply,
|
|
359
|
-
sendTextReply,
|
|
360
|
-
sendQueuedAttachments: Attachments.createTelegramQueuedAttachmentSender({
|
|
361
|
-
sendMultipart: callMultipart,
|
|
362
|
-
sendTextReply,
|
|
363
|
-
recordRuntimeEvent: runtimeEvents.record,
|
|
364
|
-
}),
|
|
365
|
-
planOutboundReply: OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
366
|
-
buttonActionStore,
|
|
367
|
-
),
|
|
368
|
-
sendOutboundReplyArtifacts: OutboundHandlers.createTelegramOutboundReplyArtifactSender({
|
|
369
|
-
execCommand: CommandTemplates.execCommandTemplate,
|
|
370
|
-
sendMultipart: callMultipart,
|
|
371
|
-
sendTextReply,
|
|
372
|
-
getHandlers: configStore.getOutboundHandlers,
|
|
373
|
-
recordRuntimeEvent: runtimeEvents.record,
|
|
374
|
-
}),
|
|
375
|
-
getActiveToolExecutions: bridgeRuntime.lifecycle.getActiveToolExecutions,
|
|
376
|
-
setActiveToolExecutions: bridgeRuntime.lifecycle.setActiveToolExecutions,
|
|
377
|
-
triggerPendingModelSwitchAbort: modelSwitchController.triggerPendingAbort,
|
|
378
|
-
}),
|
|
379
404
|
onMessageStart: previewRuntime.onMessageStart,
|
|
380
405
|
onMessageUpdate: previewRuntime.onMessageUpdate,
|
|
381
406
|
});
|
package/lib/config.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Owns persisted bot/session pairing state, local config storage, authorization policy, and first-user pairing side effects
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
6
7
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
8
|
import { homedir } from "node:os";
|
|
8
9
|
import { join, resolve } from "node:path";
|
|
@@ -64,12 +65,9 @@ export interface TelegramConfigStoreOptions {
|
|
|
64
65
|
export async function readTelegramConfig(
|
|
65
66
|
configPath: string,
|
|
66
67
|
): Promise<TelegramConfig> {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} catch {
|
|
71
|
-
return {};
|
|
72
|
-
}
|
|
68
|
+
if (!existsSync(configPath)) return {};
|
|
69
|
+
const content = await readFile(configPath, "utf8");
|
|
70
|
+
return JSON.parse(content) as TelegramConfig;
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
export async function writeTelegramConfig(
|
package/lib/locks.ts
CHANGED
|
@@ -246,13 +246,6 @@ export function createTelegramLockedPollingRuntime<
|
|
|
246
246
|
clearInterval(ownershipInterval);
|
|
247
247
|
ownershipInterval = undefined;
|
|
248
248
|
};
|
|
249
|
-
const updateStatusSafely = (ctx: TContext, phase: string) => {
|
|
250
|
-
try {
|
|
251
|
-
deps.updateStatus(ctx);
|
|
252
|
-
} catch (error) {
|
|
253
|
-
deps.recordRuntimeEvent?.("lock", error, { phase });
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
249
|
const suspendPolling = async () => {
|
|
257
250
|
stopOwnershipWatcher();
|
|
258
251
|
if (ownershipStop) {
|
|
@@ -261,7 +254,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
261
254
|
}
|
|
262
255
|
await deps.stopPolling();
|
|
263
256
|
};
|
|
264
|
-
const stopAfterOwnershipLoss = (
|
|
257
|
+
const stopAfterOwnershipLoss = () => {
|
|
265
258
|
if (ownershipStop) return;
|
|
266
259
|
stopOwnershipWatcher();
|
|
267
260
|
ownershipStop = deps
|
|
@@ -271,7 +264,6 @@ export function createTelegramLockedPollingRuntime<
|
|
|
271
264
|
)
|
|
272
265
|
.finally(() => {
|
|
273
266
|
ownershipStop = undefined;
|
|
274
|
-
updateStatusSafely(ctx, "ownership-loss-status");
|
|
275
267
|
});
|
|
276
268
|
};
|
|
277
269
|
const startOwnershipWatcher = (ctx: TContext) => {
|
|
@@ -279,7 +271,7 @@ export function createTelegramLockedPollingRuntime<
|
|
|
279
271
|
stopOwnershipWatcher();
|
|
280
272
|
ownershipInterval = setInterval(() => {
|
|
281
273
|
if (deps.lock.owns(owner)) return;
|
|
282
|
-
stopAfterOwnershipLoss(
|
|
274
|
+
stopAfterOwnershipLoss();
|
|
283
275
|
}, ownershipCheckMs);
|
|
284
276
|
ownershipInterval.unref?.();
|
|
285
277
|
};
|
package/lib/media.ts
CHANGED
|
@@ -59,17 +59,20 @@ export interface TelegramMediaGroupMessage {
|
|
|
59
59
|
media_group_id?: string;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export interface TelegramMediaGroupState<TMessage> {
|
|
62
|
+
export interface TelegramMediaGroupState<TMessage, TContext = unknown> {
|
|
63
63
|
messages: TMessage[];
|
|
64
|
+
context?: TContext;
|
|
64
65
|
flushTimer?: ReturnType<typeof setTimeout>;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
export interface TelegramMediaGroupController<
|
|
68
69
|
TMessage extends TelegramMediaGroupMessage,
|
|
70
|
+
TContext = unknown,
|
|
69
71
|
> {
|
|
70
72
|
queueMessage: (options: {
|
|
71
73
|
message: TMessage;
|
|
72
|
-
|
|
74
|
+
context?: TContext;
|
|
75
|
+
dispatchMessages: (messages: TMessage[], ctx?: TContext) => void;
|
|
73
76
|
}) => boolean;
|
|
74
77
|
removeMessages: (messageIds: number[]) => number;
|
|
75
78
|
clear: () => void;
|
|
@@ -79,7 +82,7 @@ export interface TelegramMediaGroupDispatchRuntimeDeps<
|
|
|
79
82
|
TMessage extends TelegramMediaGroupMessage,
|
|
80
83
|
TContext,
|
|
81
84
|
> {
|
|
82
|
-
mediaGroups: TelegramMediaGroupController<TMessage>;
|
|
85
|
+
mediaGroups: TelegramMediaGroupController<TMessage, TContext>;
|
|
83
86
|
dispatchMessages: (messages: TMessage[], ctx: TContext) => Promise<void>;
|
|
84
87
|
}
|
|
85
88
|
|
|
@@ -249,7 +252,7 @@ export function getTelegramMediaGroupKey(
|
|
|
249
252
|
export function removePendingTelegramMediaGroupMessages<
|
|
250
253
|
TMessage extends TelegramMediaGroupMessage,
|
|
251
254
|
>(
|
|
252
|
-
groups: Map<string, TelegramMediaGroupState<TMessage>>,
|
|
255
|
+
groups: Map<string, TelegramMediaGroupState<TMessage, unknown>>,
|
|
253
256
|
messageIds: number[],
|
|
254
257
|
clearTimer: (timer: ReturnType<typeof setTimeout>) => void,
|
|
255
258
|
): number {
|
|
@@ -273,24 +276,27 @@ export function removePendingTelegramMediaGroupMessages<
|
|
|
273
276
|
|
|
274
277
|
export function queueTelegramMediaGroupMessage<
|
|
275
278
|
TMessage extends TelegramMediaGroupMessage,
|
|
279
|
+
TContext = unknown,
|
|
276
280
|
>(options: {
|
|
277
281
|
message: TMessage;
|
|
278
|
-
|
|
282
|
+
context?: TContext;
|
|
283
|
+
groups: Map<string, TelegramMediaGroupState<TMessage, TContext>>;
|
|
279
284
|
debounceMs: number;
|
|
280
285
|
setTimer: (callback: () => void, ms: number) => ReturnType<typeof setTimeout>;
|
|
281
286
|
clearTimer: (timer: ReturnType<typeof setTimeout>) => void;
|
|
282
|
-
dispatchMessages: (messages: TMessage[]) => void;
|
|
287
|
+
dispatchMessages: (messages: TMessage[], ctx?: TContext) => void;
|
|
283
288
|
}): boolean {
|
|
284
289
|
const key = getTelegramMediaGroupKey(options.message);
|
|
285
290
|
if (!key) return false;
|
|
286
291
|
const existing = options.groups.get(key) ?? { messages: [] };
|
|
287
292
|
existing.messages.push(options.message);
|
|
293
|
+
existing.context = options.context;
|
|
288
294
|
if (existing.flushTimer) options.clearTimer(existing.flushTimer);
|
|
289
295
|
existing.flushTimer = options.setTimer(() => {
|
|
290
296
|
const state = options.groups.get(key);
|
|
291
297
|
options.groups.delete(key);
|
|
292
298
|
if (!state) return;
|
|
293
|
-
options.dispatchMessages(state.messages);
|
|
299
|
+
options.dispatchMessages(state.messages, state.context);
|
|
294
300
|
}, options.debounceMs);
|
|
295
301
|
options.groups.set(key, existing);
|
|
296
302
|
return true;
|
|
@@ -298,10 +304,11 @@ export function queueTelegramMediaGroupMessage<
|
|
|
298
304
|
|
|
299
305
|
export function createTelegramMediaGroupController<
|
|
300
306
|
TMessage extends TelegramMediaGroupMessage,
|
|
307
|
+
TContext = unknown,
|
|
301
308
|
>(
|
|
302
309
|
options: TelegramMediaGroupControllerOptions = {},
|
|
303
|
-
): TelegramMediaGroupController<TMessage> {
|
|
304
|
-
const groups = new Map<string, TelegramMediaGroupState<TMessage>>();
|
|
310
|
+
): TelegramMediaGroupController<TMessage, TContext> {
|
|
311
|
+
const groups = new Map<string, TelegramMediaGroupState<TMessage, TContext>>();
|
|
305
312
|
const debounceMs = options.debounceMs ?? TELEGRAM_MEDIA_GROUP_DEBOUNCE_MS;
|
|
306
313
|
const setTimer =
|
|
307
314
|
options.setTimer ??
|
|
@@ -309,9 +316,10 @@ export function createTelegramMediaGroupController<
|
|
|
309
316
|
setTimeout(callback, ms));
|
|
310
317
|
const clearTimer = options.clearTimer ?? clearTimeout;
|
|
311
318
|
return {
|
|
312
|
-
queueMessage: ({ message, dispatchMessages }) =>
|
|
319
|
+
queueMessage: ({ message, context, dispatchMessages }) =>
|
|
313
320
|
queueTelegramMediaGroupMessage({
|
|
314
321
|
message,
|
|
322
|
+
context,
|
|
315
323
|
groups,
|
|
316
324
|
debounceMs,
|
|
317
325
|
setTimer,
|
|
@@ -339,8 +347,11 @@ export function createTelegramMediaGroupDispatchRuntime<
|
|
|
339
347
|
handleMessage: async (message, ctx) => {
|
|
340
348
|
const queuedMediaGroup = deps.mediaGroups.queueMessage({
|
|
341
349
|
message,
|
|
342
|
-
|
|
343
|
-
|
|
350
|
+
context: ctx,
|
|
351
|
+
dispatchMessages: (messages, queuedCtx) => {
|
|
352
|
+
if (queuedCtx !== undefined) {
|
|
353
|
+
void deps.dispatchMessages(messages, queuedCtx);
|
|
354
|
+
}
|
|
344
355
|
},
|
|
345
356
|
});
|
|
346
357
|
if (queuedMediaGroup) return;
|
package/lib/outbound-handlers.ts
CHANGED
|
@@ -106,6 +106,16 @@ interface TelegramTopLevelFenceState {
|
|
|
106
106
|
length: number;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
function isTelegramActionCommentContent(content: string): boolean {
|
|
110
|
+
const normalizedContent = content.replace(/^\s+/, "");
|
|
111
|
+
const [head = ""] = normalizedContent.split(/\r?\n/, 1);
|
|
112
|
+
return ["telegram_voice", "telegram_button"].some((command) => {
|
|
113
|
+
if (!head.startsWith(command)) return false;
|
|
114
|
+
const nextChar = head[command.length];
|
|
115
|
+
return nextChar === undefined || /\s|:/.test(nextChar);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
109
119
|
function getMarkdownLineEnd(markdown: string, offset: number): number {
|
|
110
120
|
const newlineIndex = markdown.indexOf("\n", offset);
|
|
111
121
|
return newlineIndex === -1 ? markdown.length : newlineIndex + 1;
|
|
@@ -144,6 +154,28 @@ function isTopLevelClosingFence(
|
|
|
144
154
|
);
|
|
145
155
|
}
|
|
146
156
|
|
|
157
|
+
function collectInlineClosedTelegramActionBody(
|
|
158
|
+
markdown: string,
|
|
159
|
+
bodyStart: number,
|
|
160
|
+
commentContent: string,
|
|
161
|
+
): { content: string; end: number } | undefined {
|
|
162
|
+
const bodyLineEnd = getMarkdownLineEnd(markdown, bodyStart);
|
|
163
|
+
const bodyLine = getMarkdownLineText(markdown, bodyStart, bodyLineEnd);
|
|
164
|
+
const closeLineEnd = getMarkdownLineEnd(markdown, bodyLineEnd);
|
|
165
|
+
const closeLine = getMarkdownLineText(markdown, bodyLineEnd, closeLineEnd);
|
|
166
|
+
const hasRecoverableBody =
|
|
167
|
+
isTelegramActionCommentContent(commentContent) &&
|
|
168
|
+
bodyLine.trim() !== "" &&
|
|
169
|
+
!bodyLine.startsWith("<!--") &&
|
|
170
|
+
!bodyLine.startsWith("-->") &&
|
|
171
|
+
closeLine === "-->";
|
|
172
|
+
if (!hasRecoverableBody) return undefined;
|
|
173
|
+
return {
|
|
174
|
+
content: `${commentContent.trimEnd()}\n${bodyLine}`,
|
|
175
|
+
end: bodyLineEnd + 3,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
147
179
|
function collectTopLevelHtmlComments(markdown: string): {
|
|
148
180
|
comments: TelegramTopLevelHtmlComment[];
|
|
149
181
|
openCommentStart?: number;
|
|
@@ -168,9 +200,23 @@ function collectTopLevelHtmlComments(markdown: string): {
|
|
|
168
200
|
if (line.startsWith("<!--")) {
|
|
169
201
|
const closeIndex = markdown.indexOf("-->", offset + 4);
|
|
170
202
|
if (closeIndex === -1) return { comments, openCommentStart: offset };
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
203
|
+
let end = closeIndex + 3;
|
|
204
|
+
let raw = markdown.slice(offset, end);
|
|
205
|
+
let content = raw.slice(4, -3);
|
|
206
|
+
const closeColumn = closeIndex - offset;
|
|
207
|
+
const closesOnOpeningLine = closeIndex < lineEnd;
|
|
208
|
+
const hasOnlyWhitespaceAfterClose =
|
|
209
|
+
line.slice(closeColumn + 3).trim() === "";
|
|
210
|
+
const inlineBody =
|
|
211
|
+
closesOnOpeningLine && hasOnlyWhitespaceAfterClose
|
|
212
|
+
? collectInlineClosedTelegramActionBody(markdown, lineEnd, content)
|
|
213
|
+
: undefined;
|
|
214
|
+
if (inlineBody) {
|
|
215
|
+
end = inlineBody.end;
|
|
216
|
+
raw = markdown.slice(offset, end);
|
|
217
|
+
content = inlineBody.content;
|
|
218
|
+
}
|
|
219
|
+
comments.push({ raw, content, start: offset, end });
|
|
174
220
|
offset = getMarkdownLineEnd(markdown, end);
|
|
175
221
|
continue;
|
|
176
222
|
}
|
|
@@ -239,18 +285,29 @@ function parseTopLevelTelegramComment(
|
|
|
239
285
|
};
|
|
240
286
|
}
|
|
241
287
|
|
|
288
|
+
function parseTelegramCommentAttributes(input: string): Record<string, string> {
|
|
289
|
+
const attributes: Record<string, string> = {};
|
|
290
|
+
for (const match of input.matchAll(
|
|
291
|
+
/([A-Za-z_][A-Za-z0-9_-]*)=(?:"([^"]*)"|'([^']*)'|(\S+))/g,
|
|
292
|
+
)) {
|
|
293
|
+
const key = match[1];
|
|
294
|
+
const value = (match[2] ?? match[3] ?? match[4] ?? "").trim();
|
|
295
|
+
if (value) attributes[key] = value;
|
|
296
|
+
}
|
|
297
|
+
return attributes;
|
|
298
|
+
}
|
|
299
|
+
|
|
242
300
|
function parseVoiceReplyAttributes(input: string): {
|
|
243
301
|
lang?: string;
|
|
244
302
|
rate?: string;
|
|
303
|
+
text?: string;
|
|
245
304
|
} {
|
|
246
|
-
const attributes
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
return attributes;
|
|
305
|
+
const attributes = parseTelegramCommentAttributes(input);
|
|
306
|
+
return {
|
|
307
|
+
...(attributes.lang ? { lang: attributes.lang } : {}),
|
|
308
|
+
...(attributes.rate ? { rate: attributes.rate } : {}),
|
|
309
|
+
...(attributes.text ? { text: attributes.text } : {}),
|
|
310
|
+
};
|
|
254
311
|
}
|
|
255
312
|
|
|
256
313
|
function parseVoiceCommentBody(
|
|
@@ -267,7 +324,8 @@ function parseVoiceCommentBody(
|
|
|
267
324
|
if (trimmedHead.startsWith(":")) {
|
|
268
325
|
return { attrs: "", text: trimmedHead.slice(1).trim() };
|
|
269
326
|
}
|
|
270
|
-
|
|
327
|
+
const attrs = parseVoiceReplyAttributes(trimmedHead);
|
|
328
|
+
return { attrs: trimmedHead, text: attrs.text ?? "" };
|
|
271
329
|
}
|
|
272
330
|
|
|
273
331
|
function normalizeMarkdownAfterVoiceExtraction(markdown: string): string {
|
|
@@ -712,26 +770,36 @@ function normalizeMarkdownAfterButtonExtraction(markdown: string): string {
|
|
|
712
770
|
return markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
713
771
|
}
|
|
714
772
|
|
|
715
|
-
function parseButtonsCommentAttributes(input: string): {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
)
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
return attributes;
|
|
773
|
+
function parseButtonsCommentAttributes(input: string): {
|
|
774
|
+
label?: string;
|
|
775
|
+
prompt?: string;
|
|
776
|
+
} {
|
|
777
|
+
const attributes = parseTelegramCommentAttributes(input);
|
|
778
|
+
return {
|
|
779
|
+
...(attributes.label ? { label: attributes.label } : {}),
|
|
780
|
+
...(attributes.prompt ? { prompt: attributes.prompt } : {}),
|
|
781
|
+
};
|
|
725
782
|
}
|
|
726
783
|
|
|
727
784
|
function parseButtonsCommentRows(
|
|
728
785
|
head: string,
|
|
729
786
|
body: string | undefined,
|
|
730
787
|
): TelegramOutboundButtonAction[][] {
|
|
731
|
-
const
|
|
732
|
-
if (
|
|
733
|
-
|
|
734
|
-
|
|
788
|
+
const trimmedHead = head.trim();
|
|
789
|
+
if (body === undefined) {
|
|
790
|
+
if (trimmedHead.startsWith(":")) {
|
|
791
|
+
const label = trimmedHead.slice(1).trim();
|
|
792
|
+
return label ? [[{ text: label, prompt: label }]] : [];
|
|
793
|
+
}
|
|
794
|
+
const attributes = parseButtonsCommentAttributes(head);
|
|
795
|
+
return attributes.label && attributes.prompt
|
|
796
|
+
? [[{ text: attributes.label, prompt: attributes.prompt }]]
|
|
797
|
+
: [];
|
|
798
|
+
}
|
|
799
|
+
const label = parseButtonsCommentAttributes(head).label;
|
|
800
|
+
const prompt = body.trim();
|
|
801
|
+
if (!label || !prompt) return [];
|
|
802
|
+
return [[{ text: label, prompt }]];
|
|
735
803
|
}
|
|
736
804
|
|
|
737
805
|
export function createTelegramButtonActionStore(
|
package/lib/polling.ts
CHANGED
|
@@ -97,7 +97,7 @@ export interface TelegramPollingRuntimeDeps<TContext> {
|
|
|
97
97
|
setPollingController: (controller: AbortController | undefined) => void;
|
|
98
98
|
stopTypingLoop: () => unknown;
|
|
99
99
|
runPollLoop: (ctx: TContext, signal: AbortSignal) => Promise<void>;
|
|
100
|
-
updateStatus: (ctx: TContext) => void;
|
|
100
|
+
updateStatus: (ctx: TContext, message?: string) => void;
|
|
101
101
|
createAbortController?: () => AbortController;
|
|
102
102
|
}
|
|
103
103
|
|
package/lib/prompts.ts
CHANGED
|
@@ -9,15 +9,22 @@ import { TELEGRAM_PREFIX } from "./turns.ts";
|
|
|
9
9
|
const SYSTEM_PROMPT_SUFFIX = `
|
|
10
10
|
|
|
11
11
|
Telegram bridge extension is active.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- [telegram]
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
12
|
+
|
|
13
|
+
Inbound context:
|
|
14
|
+
- \`[telegram]\` marks Telegram-originated messages.
|
|
15
|
+
- \`[reply]\` is quoted context from the replied-to message, not a new instruction by itself. Use it to resolve references like "this", "it", or "that message"; the actual instruction is before [reply] unless it explicitly asks to act on the quote.
|
|
16
|
+
- \`[attachments]\` gives a base directory plus relative local files; resolve and read them as needed. \`[outputs]\` contains attachment-handler stdout such as transcriptions or extracted text for those attachments.
|
|
17
|
+
|
|
18
|
+
Telegram-visible output:
|
|
19
|
+
- Telegram is often phone-width; prefer narrow table columns because wide monospace tables can become unreadable.
|
|
20
|
+
- For requested/generated files, call tool \`telegram_attach(local_path)\`; mentioning a local path in text does not send it.
|
|
21
|
+
|
|
22
|
+
Native outbound actions:
|
|
23
|
+
- Use top-level column-zero hidden Markdown comments outside code, quotes, and lists; the bridge handles them after agent_end, so do not call or register transport/TTS/text-to-OGG tools.
|
|
24
|
+
- \`telegram_voice\`: text is synthesized through the configured outbound-handler pipeline. Use body text for multiline voice, \`<!-- telegram_voice text="Short summary" -->\` for explicit one-line voice, or \`<!-- telegram_voice: Short summary -->\` for one-line voice with no attributes. A companion summary is optional, no specific summary format is required. Keep it TTS-friendly; avoid raw Markdown, code, formulas, tables, or long lists.
|
|
25
|
+
- \`telegram_button\`: callback prompt is routed back as a normal Telegram turn. Use \`<!-- telegram_button: OK -->\` when prompt equals label, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\` for one-line prompts, or body form \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\` for multiline prompts.
|
|
26
|
+
- If only hidden action comments would remain, add visible parent text like "Choose one:".
|
|
27
|
+
`;
|
|
21
28
|
|
|
22
29
|
export function buildTelegramBridgeSystemPrompt(options: {
|
|
23
30
|
prompt: string;
|
package/lib/queue.ts
CHANGED
|
@@ -785,6 +785,9 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
785
785
|
resetRuntimeState: () => void;
|
|
786
786
|
updateStatus: (ctx: TContext) => void;
|
|
787
787
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
788
|
+
requestDeferredDispatchNextQueuedTelegramTurn: (
|
|
789
|
+
dispatch: (ctx: TContext) => void,
|
|
790
|
+
) => void;
|
|
788
791
|
clearPreview: (chatId: number) => Promise<void>;
|
|
789
792
|
setPreviewPendingText: (text: string) => void;
|
|
790
793
|
finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<TTurn>["finalizeMarkdownPreview"];
|
|
@@ -882,7 +885,9 @@ export function createTelegramAgentEndHook<
|
|
|
882
885
|
resetRuntimeState: deps.resetRuntimeState,
|
|
883
886
|
updateStatus: () => deps.updateStatus(ctx),
|
|
884
887
|
dispatchNextQueuedTelegramTurn: () => {
|
|
885
|
-
|
|
888
|
+
deps.requestDeferredDispatchNextQueuedTelegramTurn(
|
|
889
|
+
deps.dispatchNextQueuedTelegramTurn,
|
|
890
|
+
);
|
|
886
891
|
},
|
|
887
892
|
clearPreview: deps.clearPreview,
|
|
888
893
|
setPreviewPendingText: deps.setPreviewPendingText,
|
|
@@ -1020,15 +1025,18 @@ export interface TelegramSessionStateApplier<TQueueItem, TModel> {
|
|
|
1020
1025
|
applyShutdownState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
|
|
1021
1026
|
}
|
|
1022
1027
|
|
|
1023
|
-
export interface TelegramSessionStartRuntimeDeps<TModel = unknown> {
|
|
1028
|
+
export interface TelegramSessionStartRuntimeDeps<TContext, TModel = unknown> {
|
|
1029
|
+
ctx: TContext;
|
|
1024
1030
|
currentModel: TModel | undefined;
|
|
1025
1031
|
loadConfig: () => Promise<void>;
|
|
1026
1032
|
applyState: (state: TelegramSessionStartState<TModel>) => void;
|
|
1033
|
+
bindDeferredDispatchContext?: (ctx: TContext) => void;
|
|
1027
1034
|
prepareTempDir: () => Promise<unknown>;
|
|
1028
1035
|
updateStatus: () => void;
|
|
1029
1036
|
}
|
|
1030
1037
|
|
|
1031
1038
|
export interface TelegramSessionShutdownRuntimeDeps<TQueueItem> {
|
|
1039
|
+
unbindDeferredDispatchContext?: () => void;
|
|
1032
1040
|
applyState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
|
|
1033
1041
|
clearPendingMediaGroups: () => void;
|
|
1034
1042
|
clearModelMenuState: () => void;
|
|
@@ -1047,8 +1055,10 @@ export interface TelegramSessionLifecycleHookRuntimeDeps<
|
|
|
1047
1055
|
getCurrentModel: (ctx: TContext) => TModel | undefined;
|
|
1048
1056
|
loadConfig: () => Promise<void>;
|
|
1049
1057
|
applySessionStartState: (state: TelegramSessionStartState<TModel>) => void;
|
|
1058
|
+
bindDeferredDispatchContext?: (ctx: TContext) => void;
|
|
1050
1059
|
prepareTempDir: () => Promise<unknown>;
|
|
1051
1060
|
updateStatus: (ctx: TContext) => void;
|
|
1061
|
+
unbindDeferredDispatchContext?: () => void;
|
|
1052
1062
|
applySessionShutdownState: (
|
|
1053
1063
|
state: TelegramSessionShutdownState<TQueueItem>,
|
|
1054
1064
|
) => void;
|
|
@@ -1185,18 +1195,20 @@ export function buildTelegramSessionShutdownState<
|
|
|
1185
1195
|
};
|
|
1186
1196
|
}
|
|
1187
1197
|
|
|
1188
|
-
export async function startTelegramSessionRuntime<TModel = unknown>(
|
|
1189
|
-
deps: TelegramSessionStartRuntimeDeps<TModel>,
|
|
1198
|
+
export async function startTelegramSessionRuntime<TContext, TModel = unknown>(
|
|
1199
|
+
deps: TelegramSessionStartRuntimeDeps<TContext, TModel>,
|
|
1190
1200
|
): Promise<void> {
|
|
1191
1201
|
await deps.loadConfig();
|
|
1192
1202
|
deps.applyState(buildTelegramSessionStartState(deps.currentModel));
|
|
1193
1203
|
await deps.prepareTempDir();
|
|
1204
|
+
deps.bindDeferredDispatchContext?.(deps.ctx);
|
|
1194
1205
|
deps.updateStatus();
|
|
1195
1206
|
}
|
|
1196
1207
|
|
|
1197
1208
|
export async function shutdownTelegramSessionRuntime<TQueueItem>(
|
|
1198
1209
|
deps: TelegramSessionShutdownRuntimeDeps<TQueueItem>,
|
|
1199
1210
|
): Promise<void> {
|
|
1211
|
+
deps.unbindDeferredDispatchContext?.();
|
|
1200
1212
|
deps.applyState(buildTelegramSessionShutdownState<TQueueItem>());
|
|
1201
1213
|
deps.clearPendingMediaGroups();
|
|
1202
1214
|
deps.clearModelMenuState();
|
|
@@ -1235,8 +1247,10 @@ export function createTelegramSessionLifecycleRuntime<
|
|
|
1235
1247
|
getCurrentModel: deps.getCurrentModel,
|
|
1236
1248
|
loadConfig: deps.loadConfig,
|
|
1237
1249
|
applySessionStartState: stateApplier.applyStartState,
|
|
1250
|
+
bindDeferredDispatchContext: deps.bindDeferredDispatchContext,
|
|
1238
1251
|
prepareTempDir: deps.prepareTempDir,
|
|
1239
1252
|
updateStatus: deps.updateStatus,
|
|
1253
|
+
unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
|
|
1240
1254
|
applySessionShutdownState: stateApplier.applyShutdownState,
|
|
1241
1255
|
clearPendingMediaGroups: deps.clearPendingMediaGroups,
|
|
1242
1256
|
clearModelMenuState: deps.clearModelMenuState,
|
|
@@ -1261,9 +1275,11 @@ export function createTelegramSessionLifecycleHooks<
|
|
|
1261
1275
|
): Promise<void> => {
|
|
1262
1276
|
try {
|
|
1263
1277
|
await startTelegramSessionRuntime({
|
|
1278
|
+
ctx,
|
|
1264
1279
|
currentModel: deps.getCurrentModel(ctx),
|
|
1265
1280
|
loadConfig: deps.loadConfig,
|
|
1266
1281
|
applyState: deps.applySessionStartState,
|
|
1282
|
+
bindDeferredDispatchContext: deps.bindDeferredDispatchContext,
|
|
1267
1283
|
prepareTempDir: deps.prepareTempDir,
|
|
1268
1284
|
updateStatus: () => deps.updateStatus(ctx),
|
|
1269
1285
|
});
|
|
@@ -1275,6 +1291,7 @@ export function createTelegramSessionLifecycleHooks<
|
|
|
1275
1291
|
onSessionShutdown: async (): Promise<void> => {
|
|
1276
1292
|
try {
|
|
1277
1293
|
await shutdownTelegramSessionRuntime<TQueueItem>({
|
|
1294
|
+
unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
|
|
1278
1295
|
applyState: deps.applySessionShutdownState,
|
|
1279
1296
|
clearPendingMediaGroups: deps.clearPendingMediaGroups,
|
|
1280
1297
|
clearModelMenuState: deps.clearModelMenuState,
|
|
@@ -1490,6 +1507,68 @@ export async function executeTelegramControlItemRuntime<TContext>(
|
|
|
1490
1507
|
}
|
|
1491
1508
|
}
|
|
1492
1509
|
|
|
1510
|
+
// --- Deferred Dispatch Runtime ---
|
|
1511
|
+
|
|
1512
|
+
export interface TelegramDeferredQueueDispatchRuntimeDeps extends TelegramRuntimeEventRecorderPort {
|
|
1513
|
+
delayMs?: number;
|
|
1514
|
+
setTimer?: (
|
|
1515
|
+
callback: () => void,
|
|
1516
|
+
ms: number,
|
|
1517
|
+
) => ReturnType<typeof setTimeout>;
|
|
1518
|
+
clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
export interface TelegramDeferredQueueDispatchRuntime<TContext = unknown> {
|
|
1522
|
+
bind: (ctx: TContext) => void;
|
|
1523
|
+
unbind: () => void;
|
|
1524
|
+
isBound: () => boolean;
|
|
1525
|
+
request: (dispatchNextQueuedTelegramTurn: (ctx: TContext) => void) => void;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
export function createTelegramDeferredQueueDispatchRuntime<TContext = unknown>(
|
|
1529
|
+
deps: TelegramDeferredQueueDispatchRuntimeDeps = {},
|
|
1530
|
+
): TelegramDeferredQueueDispatchRuntime<TContext> {
|
|
1531
|
+
let boundContext: TContext | undefined;
|
|
1532
|
+
let generation = 0;
|
|
1533
|
+
const timers = new Set<ReturnType<typeof setTimeout>>();
|
|
1534
|
+
const delayMs = deps.delayMs ?? 0;
|
|
1535
|
+
const setTimer =
|
|
1536
|
+
deps.setTimer ??
|
|
1537
|
+
((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
|
|
1538
|
+
setTimeout(callback, ms));
|
|
1539
|
+
const clearTimer =
|
|
1540
|
+
deps.clearTimer ??
|
|
1541
|
+
((timer: ReturnType<typeof setTimeout>): void => clearTimeout(timer));
|
|
1542
|
+
const clearTimers = (): void => {
|
|
1543
|
+
for (const timer of timers) clearTimer(timer);
|
|
1544
|
+
timers.clear();
|
|
1545
|
+
};
|
|
1546
|
+
return {
|
|
1547
|
+
bind: (ctx) => {
|
|
1548
|
+
boundContext = ctx;
|
|
1549
|
+
generation += 1;
|
|
1550
|
+
},
|
|
1551
|
+
unbind: () => {
|
|
1552
|
+
boundContext = undefined;
|
|
1553
|
+
generation += 1;
|
|
1554
|
+
clearTimers();
|
|
1555
|
+
},
|
|
1556
|
+
isBound: () => boundContext !== undefined,
|
|
1557
|
+
request: (dispatchNextQueuedTelegramTurn) => {
|
|
1558
|
+
if (boundContext === undefined) return;
|
|
1559
|
+
const scheduledGeneration = generation;
|
|
1560
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
1561
|
+
timer = setTimer(() => {
|
|
1562
|
+
timers.delete(timer);
|
|
1563
|
+
if (generation !== scheduledGeneration || boundContext === undefined)
|
|
1564
|
+
return;
|
|
1565
|
+
dispatchNextQueuedTelegramTurn(boundContext);
|
|
1566
|
+
}, delayMs);
|
|
1567
|
+
timers.add(timer);
|
|
1568
|
+
},
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1493
1572
|
// --- Dispatch Runtime ---
|
|
1494
1573
|
|
|
1495
1574
|
export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
|
|
@@ -1516,6 +1595,7 @@ export interface TelegramQueueDispatchControllerDeps<
|
|
|
1516
1595
|
getQueuedItems: () => TelegramQueueItem<TContext>[];
|
|
1517
1596
|
setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
|
|
1518
1597
|
canDispatch: (ctx: TContext) => boolean;
|
|
1598
|
+
hasDispatchContext?: () => boolean;
|
|
1519
1599
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
1520
1600
|
sendTextReply: TelegramControlRuntimeDeps<TContext>["sendTextReply"];
|
|
1521
1601
|
onPromptDispatchStart: (ctx: TContext, chatId: number) => void;
|
|
@@ -1567,6 +1647,7 @@ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
|
|
|
1567
1647
|
isIdle: deps.isIdle,
|
|
1568
1648
|
hasPendingMessages: deps.hasPendingMessages,
|
|
1569
1649
|
}),
|
|
1650
|
+
hasDispatchContext: deps.hasDispatchContext,
|
|
1570
1651
|
updateStatus: deps.updateStatus,
|
|
1571
1652
|
sendTextReply: deps.sendTextReply,
|
|
1572
1653
|
onPromptDispatchStart: deps.onPromptDispatchStart,
|
|
@@ -1582,6 +1663,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
1582
1663
|
let controlDispatchPending = false;
|
|
1583
1664
|
const controller: TelegramQueueDispatchController<TContext> = {
|
|
1584
1665
|
dispatchNext: (ctx) => {
|
|
1666
|
+
if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
|
|
1585
1667
|
if (controlDispatchPending) {
|
|
1586
1668
|
deps.updateStatus(ctx);
|
|
1587
1669
|
return;
|
|
@@ -1603,6 +1685,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
1603
1685
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1604
1686
|
onSettled: () => {
|
|
1605
1687
|
controlDispatchPending = false;
|
|
1688
|
+
if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
|
|
1606
1689
|
deps.updateStatus(ctx);
|
|
1607
1690
|
controller.dispatchNext(ctx);
|
|
1608
1691
|
},
|
package/lib/routing.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
41
41
|
>;
|
|
42
42
|
bridgeRuntime: TelegramBridgeRuntime;
|
|
43
43
|
activeTurnRuntime: Queue.TelegramActiveTurnStore;
|
|
44
|
-
mediaGroupRuntime: Media.TelegramMediaGroupController<TMessage>;
|
|
44
|
+
mediaGroupRuntime: Media.TelegramMediaGroupController<TMessage, TContext>;
|
|
45
45
|
telegramQueueStore: Queue.TelegramQueueStateStore<TContext>;
|
|
46
46
|
queueMutationRuntime: Queue.TelegramQueueMutationController<TContext>;
|
|
47
47
|
modelMenuRuntime: Menu.TelegramModelMenuRuntime<TModel>;
|
package/lib/runtime.ts
CHANGED