@llblab/pi-telegram 0.54.3 → 0.55.0
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/AGENTS.md +4 -2
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +21 -0
- package/README.md +5 -3
- package/dist/lib/activity.d.ts +5 -1
- package/dist/lib/activity.js +31 -2
- package/dist/lib/bindings.d.ts +5 -1
- package/dist/lib/bindings.js +9 -2
- package/dist/lib/bus-api.d.ts +2 -2
- package/dist/lib/bus-api.js +7 -2
- package/dist/lib/bus-follower.d.ts +49 -3
- package/dist/lib/bus-follower.js +313 -146
- package/dist/lib/bus-leader.d.ts +3 -0
- package/dist/lib/bus-leader.js +99 -18
- package/dist/lib/bus.d.ts +5 -3
- package/dist/lib/bus.js +13 -10
- package/dist/lib/delivery.d.ts +4 -0
- package/dist/lib/delivery.js +48 -15
- package/dist/lib/extension.js +56 -6
- package/dist/lib/lifecycle.d.ts +68 -0
- package/dist/lib/lifecycle.js +178 -58
- package/dist/lib/machine.d.ts +23 -0
- package/dist/lib/machine.js +37 -0
- package/dist/lib/media.d.ts +114 -28
- package/dist/lib/media.js +292 -119
- package/dist/lib/model.d.ts +54 -15
- package/dist/lib/model.js +98 -53
- package/dist/lib/polling.js +2 -0
- package/dist/lib/preview.d.ts +128 -113
- package/dist/lib/preview.js +448 -366
- package/dist/lib/queue.d.ts +2 -2
- package/dist/lib/queue.js +3 -3
- package/dist/lib/routing.d.ts +14 -2
- package/dist/lib/routing.js +546 -207
- package/dist/lib/runtime.d.ts +77 -11
- package/dist/lib/runtime.js +174 -92
- package/dist/lib/sections.js +0 -1
- package/dist/lib/telegram-api.d.ts +11 -9
- package/dist/lib/telegram-api.js +53 -4
- package/dist/lib/text-groups.d.ts +0 -26
- package/dist/lib/text-groups.js +51 -98
- package/dist/lib/thread-display.js +14 -6
- package/dist/lib/threads.d.ts +20 -5
- package/dist/lib/threads.js +111 -32
- package/dist/lib/turns.d.ts +2 -0
- package/dist/lib/turns.js +6 -1
- package/dist/lib/updates.d.ts +11 -4
- package/dist/lib/updates.js +54 -19
- package/dist/package.json +1 -1
- package/docs/activity.md +1 -1
- package/docs/architecture.md +16 -15
- package/docs/delivery.md +7 -5
- package/docs/multi-instance-bus.md +18 -8
- package/docs/public-api.md +2 -2
- package/docs/ui-style.md +8 -6
- package/lib/activity.ts +29 -5
- package/lib/bindings.ts +13 -1
- package/lib/bus-api.ts +11 -3
- package/lib/bus-follower.ts +417 -203
- package/lib/bus-leader.ts +88 -14
- package/lib/bus.ts +17 -15
- package/lib/delivery.ts +47 -19
- package/lib/extension.ts +59 -9
- package/lib/lifecycle.ts +229 -55
- package/lib/machine.ts +60 -0
- package/lib/media.ts +438 -172
- package/lib/model.ts +169 -74
- package/lib/polling.ts +2 -0
- package/lib/preview.ts +715 -566
- package/lib/queue.ts +5 -5
- package/lib/routing.ts +644 -283
- package/lib/runtime.ts +260 -104
- package/lib/sections.ts +0 -2
- package/lib/telegram-api.ts +61 -8
- package/lib/text-groups.ts +75 -136
- package/lib/thread-display.ts +15 -4
- package/lib/threads.ts +130 -38
- package/lib/turns.ts +8 -1
- package/lib/updates.ts +58 -21
- package/package.json +1 -1
package/dist/lib/runtime.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Zones: pi agent runtime state, telegram session, shared coordination
|
|
4
4
|
* Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
|
|
5
5
|
*/
|
|
6
|
+
import * as Machine from "./machine.ts";
|
|
6
7
|
export interface TelegramRuntimeQueueCounters {
|
|
7
8
|
nextQueuedTelegramItemOrder: number;
|
|
8
9
|
nextQueuedTelegramControlOrder: number;
|
|
@@ -16,10 +17,6 @@ export interface TelegramRuntimeLifecycleFlags {
|
|
|
16
17
|
}
|
|
17
18
|
export interface TelegramBridgeRuntimeState extends TelegramRuntimeQueueCounters, TelegramRuntimeLifecycleFlags {
|
|
18
19
|
abortHandler?: () => void;
|
|
19
|
-
typingInterval?: ReturnType<typeof setInterval>;
|
|
20
|
-
typingInFlight?: Promise<void>;
|
|
21
|
-
typingLoopDeps?: TelegramTypingLoopDeps;
|
|
22
|
-
typingLoopKey?: string;
|
|
23
20
|
}
|
|
24
21
|
export interface TelegramRuntimeQueuePort {
|
|
25
22
|
syncCounters: (counters: Partial<TelegramRuntimeQueueCounters>) => void;
|
|
@@ -54,7 +51,8 @@ export interface TelegramRuntimeAbortPort {
|
|
|
54
51
|
export interface TelegramRuntimeTypingPort {
|
|
55
52
|
start: (deps: TelegramTypingLoopDeps) => boolean;
|
|
56
53
|
stop: () => boolean;
|
|
57
|
-
|
|
54
|
+
/** Waits for the in-flight action, bounded so a never-settling request cannot hold shutdown. */
|
|
55
|
+
waitForIdle: (timeoutMs?: number) => Promise<void>;
|
|
58
56
|
}
|
|
59
57
|
export interface TelegramBridgeRuntime {
|
|
60
58
|
state: TelegramBridgeRuntimeState;
|
|
@@ -65,7 +63,7 @@ export interface TelegramBridgeRuntime {
|
|
|
65
63
|
typing: TelegramRuntimeTypingPort;
|
|
66
64
|
}
|
|
67
65
|
export declare function createTelegramBridgeRuntimeState(): TelegramBridgeRuntimeState;
|
|
68
|
-
export declare function createTelegramBridgeRuntime(state?: TelegramBridgeRuntimeState): TelegramBridgeRuntime;
|
|
66
|
+
export declare function createTelegramBridgeRuntime(state?: TelegramBridgeRuntimeState, typing?: TelegramRuntimeTypingPort): TelegramBridgeRuntime;
|
|
69
67
|
export declare function syncTelegramQueueRuntimeCounters(state: TelegramBridgeRuntimeState, counters: Partial<TelegramRuntimeQueueCounters>): void;
|
|
70
68
|
export declare function allocateTelegramQueueItemOrder(state: TelegramBridgeRuntimeState): number;
|
|
71
69
|
export declare function allocateTelegramQueueControlOrder(state: TelegramBridgeRuntimeState): number;
|
|
@@ -109,19 +107,87 @@ export interface TelegramTypingLoopStarterDeps<TContext> extends TelegramRuntime
|
|
|
109
107
|
getDefaultChatId: () => number | undefined;
|
|
110
108
|
sendTypingAction: (chatId: number, options?: {
|
|
111
109
|
message_thread_id?: number;
|
|
110
|
+
assertAuthority?: () => void;
|
|
112
111
|
}) => Promise<unknown>;
|
|
113
112
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
114
113
|
isContextActive?: (ctx: TContext) => boolean;
|
|
114
|
+
getSessionGeneration?: () => number;
|
|
115
115
|
isTransportAvailable?: () => boolean;
|
|
116
|
+
isThreadedMode?: () => boolean;
|
|
116
117
|
getTransportAuthority?: () => string | number | undefined;
|
|
117
118
|
intervalMs?: number;
|
|
118
119
|
}
|
|
119
120
|
export declare function createTelegramTypingLoopStarter<TContext>(deps: TelegramTypingLoopStarterDeps<TContext>): (ctx: TContext, chatId?: number, options?: {
|
|
120
121
|
target?: TelegramTypingLoopTarget;
|
|
121
122
|
}) => boolean;
|
|
122
|
-
|
|
123
|
-
export
|
|
124
|
-
|
|
123
|
+
/** The whole typing-loop state; only `transitionTelegramTypingLoop` produces a new one. */
|
|
124
|
+
export interface TelegramTypingMachineState {
|
|
125
|
+
/** The loop the interval serves; its callbacks live in the shell under `id`. A loop exists exactly while ticking. */
|
|
126
|
+
readonly loop: {
|
|
127
|
+
readonly id: number;
|
|
128
|
+
readonly key: string;
|
|
129
|
+
readonly chatId: number;
|
|
130
|
+
readonly threadParams?: {
|
|
131
|
+
readonly message_thread_id: number;
|
|
132
|
+
};
|
|
133
|
+
} | null;
|
|
134
|
+
/** The action in flight; a tick waits for it, and replacing or stopping the loop forgets it. */
|
|
135
|
+
readonly inFlight: number | null;
|
|
136
|
+
readonly nextSendId: number;
|
|
137
|
+
}
|
|
138
|
+
export type TelegramTypingEvent = {
|
|
139
|
+
type: "started";
|
|
140
|
+
/** Shell-assigned, so the shell can hold the caller's callbacks before effects run. */
|
|
141
|
+
loopId: number;
|
|
142
|
+
chatId: number | undefined;
|
|
143
|
+
key: string;
|
|
144
|
+
threadParams?: {
|
|
145
|
+
message_thread_id: number;
|
|
146
|
+
};
|
|
147
|
+
intervalMs: number;
|
|
148
|
+
/** The caller's `shouldContinue`, sampled at dispatch. */
|
|
149
|
+
continues: boolean;
|
|
150
|
+
} | {
|
|
151
|
+
type: "ticked";
|
|
152
|
+
continues: boolean;
|
|
153
|
+
} | {
|
|
154
|
+
type: "sendSettled";
|
|
155
|
+
sendId: number;
|
|
156
|
+
} | {
|
|
157
|
+
type: "stopRequested";
|
|
158
|
+
};
|
|
159
|
+
export type TelegramTypingEffect = {
|
|
160
|
+
type: "send";
|
|
161
|
+
loopId: number;
|
|
162
|
+
sendId: number;
|
|
163
|
+
chatId: number;
|
|
164
|
+
threadParams?: {
|
|
165
|
+
message_thread_id: number;
|
|
166
|
+
};
|
|
167
|
+
} | {
|
|
168
|
+
type: "armInterval";
|
|
169
|
+
intervalMs: number;
|
|
170
|
+
} | {
|
|
171
|
+
type: "cancelInterval";
|
|
172
|
+
} | {
|
|
173
|
+
type: "loopEnded";
|
|
174
|
+
loopId: number;
|
|
175
|
+
};
|
|
176
|
+
export interface TelegramTypingStep extends Machine.TelegramMachineStep<TelegramTypingMachineState, TelegramTypingEffect> {
|
|
177
|
+
/** `start`: a loop began or moved to a new target. `stop`: a running loop ended. */
|
|
178
|
+
result: boolean;
|
|
179
|
+
}
|
|
180
|
+
export declare function createTelegramTypingMachineState(): TelegramTypingMachineState;
|
|
181
|
+
/**
|
|
182
|
+
* Pure typing-loop transition. Every start replaces the caller's callbacks, even on the same target, and forgets the
|
|
183
|
+
* old in-flight action; only a new target sends at once on a running loop, whose interval keeps its phase.
|
|
184
|
+
*/
|
|
185
|
+
export declare function transitionTelegramTypingLoop(state: TelegramTypingMachineState, event: TelegramTypingEvent): TelegramTypingStep;
|
|
186
|
+
/**
|
|
187
|
+
* The shell around `transitionTelegramTypingLoop`: it holds each loop's callbacks, samples `shouldContinue` into
|
|
188
|
+
* events, owns the interval and turns every action outcome into `sendSettled`.
|
|
189
|
+
*/
|
|
190
|
+
export declare function createTelegramTypingLoop(): TelegramRuntimeTypingPort;
|
|
125
191
|
export declare function createTelegramContextAbortHandlerSetter<TContext extends {
|
|
126
192
|
abort: () => void;
|
|
127
193
|
}>(abort: Pick<TelegramRuntimeAbortPort, "setHandler">): (ctx: TContext) => void;
|
|
@@ -149,11 +215,11 @@ export interface TelegramPromptDispatchRuntime<TContext> {
|
|
|
149
215
|
startTypingLoop: (ctx: TContext, chatId?: number, options?: {
|
|
150
216
|
target?: TelegramTypingLoopTarget;
|
|
151
217
|
}) => boolean | void;
|
|
152
|
-
onPromptDispatchStart: (ctx: TContext, chatId?: number) => void;
|
|
218
|
+
onPromptDispatchStart: (ctx: TContext, chatId?: number, target?: TelegramTypingLoopTarget) => void;
|
|
153
219
|
onPromptDispatchFailure: (ctx: TContext, message: string) => void;
|
|
154
220
|
}
|
|
155
221
|
export declare function createTelegramPromptDispatchRuntime<TContext>(deps: TelegramPromptDispatchRuntimeDeps<TContext>): TelegramPromptDispatchRuntime<TContext>;
|
|
156
222
|
export declare function createTelegramPromptDispatchLifecycle<TContext>(deps: TelegramPromptDispatchLifecycleDeps<TContext>): {
|
|
157
|
-
onPromptDispatchStart: (ctx: TContext, chatId?: number) => void;
|
|
223
|
+
onPromptDispatchStart: (ctx: TContext, chatId?: number, target?: TelegramTypingLoopTarget) => void;
|
|
158
224
|
onPromptDispatchFailure: (ctx: TContext, message: string) => void;
|
|
159
225
|
};
|
package/dist/lib/runtime.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Zones: pi agent runtime state, telegram session, shared coordination
|
|
4
4
|
* Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
|
|
5
5
|
*/
|
|
6
|
+
import * as Machine from "./machine.js";
|
|
6
7
|
const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 3_000;
|
|
7
8
|
const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS = 250;
|
|
8
9
|
export function createTelegramBridgeRuntimeState() {
|
|
@@ -16,7 +17,7 @@ export function createTelegramBridgeRuntimeState() {
|
|
|
16
17
|
setupInProgress: false,
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
|
-
export function createTelegramBridgeRuntime(state = createTelegramBridgeRuntimeState()) {
|
|
20
|
+
export function createTelegramBridgeRuntime(state = createTelegramBridgeRuntimeState(), typing = createTelegramTypingLoop()) {
|
|
20
21
|
return {
|
|
21
22
|
state,
|
|
22
23
|
queue: {
|
|
@@ -49,11 +50,7 @@ export function createTelegramBridgeRuntime(state = createTelegramBridgeRuntimeS
|
|
|
49
50
|
getHandler: () => getTelegramAbortHandler(state),
|
|
50
51
|
abortTurn: () => abortTelegramTurn(state),
|
|
51
52
|
},
|
|
52
|
-
typing
|
|
53
|
-
start: (deps) => startTelegramTypingLoop(state, deps),
|
|
54
|
-
stop: () => stopTelegramTypingLoop(state),
|
|
55
|
-
waitForIdle: () => waitForTelegramTypingLoopIdle(state),
|
|
56
|
-
},
|
|
53
|
+
typing,
|
|
57
54
|
};
|
|
58
55
|
}
|
|
59
56
|
export function syncTelegramQueueRuntimeCounters(state, counters) {
|
|
@@ -148,12 +145,6 @@ export function abortTelegramTurn(state) {
|
|
|
148
145
|
state.abortHandler();
|
|
149
146
|
return true;
|
|
150
147
|
}
|
|
151
|
-
function getTelegramTypingLoopThreadParams(target) {
|
|
152
|
-
const threadId = target?.threadId;
|
|
153
|
-
return Number.isInteger(threadId)
|
|
154
|
-
? { message_thread_id: threadId }
|
|
155
|
-
: undefined;
|
|
156
|
-
}
|
|
157
148
|
function updateTelegramRuntimeStatusSafely(updateStatus, ctx, options) {
|
|
158
149
|
try {
|
|
159
150
|
updateStatus(ctx, options.error);
|
|
@@ -171,34 +162,47 @@ function updateTelegramRuntimeStatusSafely(updateStatus, ctx, options) {
|
|
|
171
162
|
}
|
|
172
163
|
export function createTelegramTypingLoopStarter(deps) {
|
|
173
164
|
return (ctx, chatId, options) => {
|
|
165
|
+
const sessionGeneration = deps.getSessionGeneration?.();
|
|
174
166
|
const transportAuthority = deps.getTransportAuthority?.();
|
|
167
|
+
const isContextCurrent = () => deps.isContextActive?.(ctx) !== false &&
|
|
168
|
+
(!deps.getSessionGeneration || Object.is(deps.getSessionGeneration(), sessionGeneration));
|
|
175
169
|
const hasTransport = () => deps.getTransportAuthority
|
|
176
170
|
? transportAuthority !== undefined &&
|
|
177
171
|
Object.is(deps.getTransportAuthority(), transportAuthority)
|
|
178
172
|
: deps.isTransportAvailable?.() !== false;
|
|
179
|
-
|
|
173
|
+
const target = options?.target ? { ...options.target } : undefined;
|
|
174
|
+
const canContinue = () => isContextCurrent() && hasTransport() &&
|
|
175
|
+
(deps.isThreadedMode?.() !== true ||
|
|
176
|
+
(target?.threadId !== undefined && Number.isSafeInteger(target.threadId) && target.threadId > 0));
|
|
177
|
+
// Queue dispatch precedes agent_start: only an explicit destination permits Threaded presence, never chat fallback.
|
|
178
|
+
if (!canContinue())
|
|
180
179
|
return false;
|
|
181
180
|
let active = true;
|
|
182
181
|
return deps.typing.start({
|
|
183
182
|
chatId: chatId ?? deps.getDefaultChatId(),
|
|
184
|
-
target
|
|
183
|
+
target,
|
|
185
184
|
intervalMs: deps.intervalMs ?? TELEGRAM_TYPING_ACTION_INTERVAL_MS,
|
|
186
185
|
sendTypingAction: async (targetChatId, actionOptions) => {
|
|
187
186
|
if (!active)
|
|
188
187
|
return;
|
|
189
|
-
if (!
|
|
188
|
+
if (!canContinue()) {
|
|
190
189
|
deps.typing.stop();
|
|
191
190
|
return;
|
|
192
191
|
}
|
|
193
192
|
try {
|
|
194
|
-
await deps.sendTypingAction(targetChatId,
|
|
193
|
+
await deps.sendTypingAction(targetChatId, {
|
|
194
|
+
...actionOptions,
|
|
195
|
+
// This stays process-local; adapters recheck it before HTTP/IPC issuance, not just loop ticks.
|
|
196
|
+
assertAuthority() {
|
|
197
|
+
if (!active || !canContinue())
|
|
198
|
+
throw new Error("Typing context authority changed.");
|
|
199
|
+
},
|
|
200
|
+
});
|
|
195
201
|
}
|
|
196
202
|
catch (error) {
|
|
197
|
-
if (deps.isContextActive?.(ctx) === false)
|
|
198
|
-
return;
|
|
199
203
|
if (!active)
|
|
200
204
|
return;
|
|
201
|
-
if (!
|
|
205
|
+
if (!canContinue()) {
|
|
202
206
|
deps.typing.stop();
|
|
203
207
|
return;
|
|
204
208
|
}
|
|
@@ -212,7 +216,7 @@ export function createTelegramTypingLoopStarter(deps) {
|
|
|
212
216
|
}
|
|
213
217
|
}
|
|
214
218
|
},
|
|
215
|
-
shouldContinue:
|
|
219
|
+
shouldContinue: canContinue,
|
|
216
220
|
onStopped: () => {
|
|
217
221
|
active = false;
|
|
218
222
|
},
|
|
@@ -223,79 +227,157 @@ function getTelegramTypingLoopKey(deps) {
|
|
|
223
227
|
const threadId = deps.target?.threadId;
|
|
224
228
|
return `${deps.chatId ?? 0}:${Number.isInteger(threadId) ? threadId : "all"}`;
|
|
225
229
|
}
|
|
226
|
-
export function
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
230
|
+
export function createTelegramTypingMachineState() {
|
|
231
|
+
return { loop: null, inFlight: null, nextSendId: 0 };
|
|
232
|
+
}
|
|
233
|
+
const typingStep = (state, result, ...effects) => ({ state, effects, result });
|
|
234
|
+
function stopTypingLoop(state) {
|
|
235
|
+
if (!state.loop)
|
|
236
|
+
return typingStep(state, false);
|
|
237
|
+
return typingStep({ ...state, loop: null, inFlight: null }, true, { type: "cancelInterval" }, { type: "loopEnded", loopId: state.loop.id });
|
|
238
|
+
}
|
|
239
|
+
/** One action per tick, never two at once; a caller that stops continuing ends the loop instead. */
|
|
240
|
+
function sendTypingAction(state, continues) {
|
|
241
|
+
const loop = state.loop;
|
|
242
|
+
if (!loop)
|
|
243
|
+
return typingStep(state, false);
|
|
244
|
+
if (!continues)
|
|
245
|
+
return stopTypingLoop(state);
|
|
246
|
+
if (state.inFlight !== null)
|
|
247
|
+
return typingStep(state, false);
|
|
248
|
+
const sendId = state.nextSendId + 1;
|
|
249
|
+
return typingStep({ ...state, inFlight: sendId, nextSendId: sendId }, false, {
|
|
250
|
+
type: "send",
|
|
251
|
+
loopId: loop.id,
|
|
252
|
+
sendId,
|
|
253
|
+
chatId: loop.chatId,
|
|
254
|
+
...(loop.threadParams ? { threadParams: loop.threadParams } : {}),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Pure typing-loop transition. Every start replaces the caller's callbacks, even on the same target, and forgets the
|
|
259
|
+
* old in-flight action; only a new target sends at once on a running loop, whose interval keeps its phase.
|
|
260
|
+
*/
|
|
261
|
+
export function transitionTelegramTypingLoop(state, event) {
|
|
262
|
+
switch (event.type) {
|
|
263
|
+
case "started": {
|
|
264
|
+
if (event.chatId === undefined || event.chatId === 0)
|
|
265
|
+
return typingStep(state, false);
|
|
266
|
+
const previous = state.loop;
|
|
267
|
+
const replaced = previous
|
|
268
|
+
? [{ type: "loopEnded", loopId: previous.id }]
|
|
269
|
+
: [];
|
|
270
|
+
const next = {
|
|
271
|
+
...state,
|
|
272
|
+
inFlight: previous ? null : state.inFlight,
|
|
273
|
+
loop: {
|
|
274
|
+
id: event.loopId,
|
|
275
|
+
key: event.key,
|
|
276
|
+
chatId: event.chatId,
|
|
277
|
+
...(event.threadParams ? { threadParams: event.threadParams } : {}),
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
if (previous?.key === event.key)
|
|
281
|
+
return typingStep(next, false, ...replaced);
|
|
282
|
+
if (previous) {
|
|
283
|
+
const sent = sendTypingAction(next, event.continues);
|
|
284
|
+
return typingStep(sent.state, true, ...replaced, ...sent.effects);
|
|
285
|
+
}
|
|
286
|
+
// A fresh loop always arms its interval; a caller that already stopped continuing ends it on the first tick.
|
|
287
|
+
const sent = event.continues
|
|
288
|
+
? sendTypingAction(next, true)
|
|
289
|
+
: typingStep(next, false);
|
|
290
|
+
return typingStep(sent.state, true, ...sent.effects, {
|
|
291
|
+
type: "armInterval",
|
|
292
|
+
intervalMs: event.intervalMs,
|
|
293
|
+
});
|
|
247
294
|
}
|
|
248
|
-
|
|
249
|
-
return;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
.then(() => undefined)
|
|
255
|
-
.catch(() => undefined);
|
|
256
|
-
state.typingInFlight = typing;
|
|
257
|
-
void typing.finally(() => {
|
|
258
|
-
if (state.typingInFlight === typing)
|
|
259
|
-
state.typingInFlight = undefined;
|
|
260
|
-
});
|
|
261
|
-
};
|
|
262
|
-
if (state.typingInterval) {
|
|
263
|
-
if (previousKey === nextKey)
|
|
264
|
-
return false;
|
|
265
|
-
sendTyping();
|
|
266
|
-
return true;
|
|
295
|
+
case "ticked":
|
|
296
|
+
return sendTypingAction(state, event.continues);
|
|
297
|
+
case "sendSettled":
|
|
298
|
+
return typingStep(state.inFlight === event.sendId ? { ...state, inFlight: null } : state, false);
|
|
299
|
+
case "stopRequested":
|
|
300
|
+
return stopTypingLoop(state);
|
|
267
301
|
}
|
|
268
|
-
sendTyping();
|
|
269
|
-
state.typingInterval = setInterval(sendTyping, deps.intervalMs);
|
|
270
|
-
state.typingInterval.unref?.();
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
export function stopTelegramTypingLoop(state) {
|
|
274
|
-
if (!state.typingInterval)
|
|
275
|
-
return false;
|
|
276
|
-
clearInterval(state.typingInterval);
|
|
277
|
-
const activeDeps = state.typingLoopDeps;
|
|
278
|
-
state.typingInterval = undefined;
|
|
279
|
-
state.typingLoopDeps = undefined;
|
|
280
|
-
state.typingLoopKey = undefined;
|
|
281
|
-
state.typingInFlight = undefined;
|
|
282
|
-
activeDeps?.onStopped?.();
|
|
283
|
-
return true;
|
|
284
302
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
303
|
+
/**
|
|
304
|
+
* The shell around `transitionTelegramTypingLoop`: it holds each loop's callbacks, samples `shouldContinue` into
|
|
305
|
+
* events, owns the interval and turns every action outcome into `sendSettled`.
|
|
306
|
+
*/
|
|
307
|
+
export function createTelegramTypingLoop() {
|
|
308
|
+
const callbacks = new Map();
|
|
309
|
+
let interval;
|
|
310
|
+
let loopIds = 0;
|
|
311
|
+
const continues = (loopId) => loopId === undefined
|
|
312
|
+
? false
|
|
313
|
+
: callbacks.get(loopId)?.shouldContinue?.() !== false;
|
|
314
|
+
const machine = Machine.createTelegramMachineRunner({
|
|
315
|
+
initial: createTelegramTypingMachineState(),
|
|
316
|
+
transition: transitionTelegramTypingLoop,
|
|
317
|
+
run(effect) {
|
|
318
|
+
switch (effect.type) {
|
|
319
|
+
case "send": {
|
|
320
|
+
const send = callbacks.get(effect.loopId)?.sendTypingAction;
|
|
321
|
+
// Failures belong to the caller's action; the loop only needs to know the action is over.
|
|
322
|
+
void Promise.resolve()
|
|
323
|
+
.then(() => send?.(effect.chatId, effect.threadParams))
|
|
324
|
+
.then(() => undefined, () => undefined)
|
|
325
|
+
.then(() => machine.dispatch({ type: "sendSettled", sendId: effect.sendId }));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
case "armInterval":
|
|
329
|
+
interval = setInterval(() => machine.dispatch({
|
|
330
|
+
type: "ticked",
|
|
331
|
+
continues: continues(machine.state.loop?.id),
|
|
332
|
+
}), effect.intervalMs);
|
|
333
|
+
interval.unref?.();
|
|
334
|
+
return;
|
|
335
|
+
case "cancelInterval":
|
|
336
|
+
clearInterval(interval);
|
|
337
|
+
interval = undefined;
|
|
338
|
+
return;
|
|
339
|
+
case "loopEnded": {
|
|
340
|
+
const ended = callbacks.get(effect.loopId);
|
|
341
|
+
callbacks.delete(effect.loopId);
|
|
342
|
+
ended?.onStopped?.();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
return {
|
|
349
|
+
start(deps) {
|
|
350
|
+
const loopId = ++loopIds;
|
|
351
|
+
callbacks.set(loopId, deps);
|
|
352
|
+
const threadId = deps.target?.threadId;
|
|
353
|
+
const started = machine.dispatch({
|
|
354
|
+
type: "started",
|
|
355
|
+
loopId,
|
|
356
|
+
chatId: deps.chatId,
|
|
357
|
+
key: getTelegramTypingLoopKey(deps),
|
|
358
|
+
...(Number.isInteger(threadId)
|
|
359
|
+
? { threadParams: { message_thread_id: threadId } }
|
|
360
|
+
: {}),
|
|
361
|
+
intervalMs: deps.intervalMs,
|
|
362
|
+
continues: continues(loopId),
|
|
363
|
+
});
|
|
364
|
+
if (machine.state.loop?.id !== loopId)
|
|
365
|
+
callbacks.delete(loopId);
|
|
366
|
+
return started.result;
|
|
367
|
+
},
|
|
368
|
+
stop: () => machine.dispatch({ type: "stopRequested" }).result,
|
|
369
|
+
async waitForIdle(timeoutMs = TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS) {
|
|
370
|
+
if (machine.state.inFlight === null)
|
|
371
|
+
return;
|
|
372
|
+
const idle = machine.waitFor((state) => state.inFlight === null);
|
|
373
|
+
await Promise.race([
|
|
374
|
+
idle,
|
|
375
|
+
timeoutMs <= 0
|
|
376
|
+
? Promise.resolve()
|
|
377
|
+
: new Promise((resolve) => setTimeout(resolve, timeoutMs)),
|
|
378
|
+
]);
|
|
379
|
+
},
|
|
380
|
+
};
|
|
299
381
|
}
|
|
300
382
|
export function createTelegramContextAbortHandlerSetter(abort) {
|
|
301
383
|
return (ctx) => {
|
|
@@ -327,9 +409,9 @@ export function createTelegramPromptDispatchRuntime(deps) {
|
|
|
327
409
|
}
|
|
328
410
|
export function createTelegramPromptDispatchLifecycle(deps) {
|
|
329
411
|
return {
|
|
330
|
-
onPromptDispatchStart: (ctx, chatId) => {
|
|
412
|
+
onPromptDispatchStart: (ctx, chatId, target) => {
|
|
331
413
|
deps.lifecycle.setDispatchPending(true);
|
|
332
|
-
deps.startTypingLoop(ctx, chatId);
|
|
414
|
+
deps.startTypingLoop(ctx, chatId, { target });
|
|
333
415
|
updateTelegramRuntimeStatusSafely(deps.updateStatus, ctx, {
|
|
334
416
|
category: "dispatch",
|
|
335
417
|
phase: "status-update",
|
package/dist/lib/sections.js
CHANGED
|
@@ -369,7 +369,9 @@ export interface TelegramApiTargetActivityRuntime {
|
|
|
369
369
|
}[];
|
|
370
370
|
listPendingChats: () => number[];
|
|
371
371
|
}
|
|
372
|
-
export declare function createTelegramApiTargetActivityRuntime(
|
|
372
|
+
export declare function createTelegramApiTargetActivityRuntime(deps?: {
|
|
373
|
+
captureDrain?: () => (() => void) | undefined;
|
|
374
|
+
}): TelegramApiTargetActivityRuntime;
|
|
373
375
|
export declare function createTelegramApiTargetTrackingClient(client: TelegramApiClient, activity: TelegramApiTargetActivityRuntime): TelegramApiClient;
|
|
374
376
|
export type TelegramApiWorkspaceAdmissionScopeLike = {
|
|
375
377
|
kind: "target";
|
|
@@ -431,6 +433,10 @@ export interface TelegramBridgeApiRuntimeDeps {
|
|
|
431
433
|
chatActionMinIntervalMs?: number;
|
|
432
434
|
chatActionMaxGates?: number;
|
|
433
435
|
}
|
|
436
|
+
/** Typing issuance guard is local authority, never Bot API body or IPC data. */
|
|
437
|
+
export interface TelegramTypingActionOptions extends Pick<TelegramApiCallOptions, "assertAuthority"> {
|
|
438
|
+
message_thread_id?: number;
|
|
439
|
+
}
|
|
434
440
|
export interface TelegramBridgeApiRuntime extends Pick<TelegramApiClient, "call" | "callMultipart"> {
|
|
435
441
|
downloadFile: (fileId: string, suggestedName: string, source?: TelegramAttachmentSource) => Promise<string>;
|
|
436
442
|
deleteWebhook: (signal?: AbortSignal) => Promise<boolean>;
|
|
@@ -442,9 +448,7 @@ export interface TelegramBridgeApiRuntime extends Pick<TelegramApiClient, "call"
|
|
|
442
448
|
sendChatAction: (chatId: number, action: string, options?: {
|
|
443
449
|
message_thread_id?: number;
|
|
444
450
|
}) => Promise<boolean>;
|
|
445
|
-
sendTypingAction: (chatId: number, options?:
|
|
446
|
-
message_thread_id?: number;
|
|
447
|
-
}) => Promise<unknown>;
|
|
451
|
+
sendTypingAction: (chatId: number, options?: TelegramTypingActionOptions) => Promise<unknown>;
|
|
448
452
|
sendRecordVoiceAction: (chatId: number, options?: {
|
|
449
453
|
message_thread_id?: number;
|
|
450
454
|
}) => Promise<unknown>;
|
|
@@ -522,11 +526,7 @@ export declare function fetchTelegramBotIdentity(botToken: string, fetchImpl?: t
|
|
|
522
526
|
export declare function callTelegramMultipart<TResponse>(botToken: string | undefined, method: string, fields: Record<string, string>, fileField: string, filePath: string, fileName: string, options?: TelegramApiCallOptions): Promise<TResponse>;
|
|
523
527
|
export declare function downloadTelegramFile(botToken: string | undefined, fileId: string, suggestedName: string, tempDir: string, options?: TelegramFileDownloadOptions): Promise<string>;
|
|
524
528
|
export declare function answerTelegramCallbackQuery(botToken: string | undefined, callbackQueryId: string, text?: string, options?: TelegramAnswerCallbackQueryOptions): Promise<void>;
|
|
525
|
-
export declare function createTelegramChatActionSender<TAction extends string>(sendChatAction: (chatId: number, action: TAction, options?:
|
|
526
|
-
message_thread_id?: number;
|
|
527
|
-
}) => Promise<unknown>, action: TAction): (chatId: number, options?: {
|
|
528
|
-
message_thread_id?: number;
|
|
529
|
-
}) => Promise<unknown>;
|
|
529
|
+
export declare function createTelegramChatActionSender<TAction extends string>(sendChatAction: (chatId: number, action: TAction, options?: TelegramTypingActionOptions) => Promise<unknown>, action: TAction): (chatId: number, options?: TelegramTypingActionOptions) => Promise<unknown>;
|
|
530
530
|
export declare function createTelegramNativeMarkdownDraftSender(deps: {
|
|
531
531
|
sendMessageDraft: TelegramBridgeApiRuntime["sendMessageDraft"];
|
|
532
532
|
sendRichMessageDraft: TelegramBridgeApiRuntime["sendRichMessageDraft"];
|
|
@@ -545,6 +545,7 @@ export type TelegramWorkspaceThreadDeletionTransport = (authorize: () => {
|
|
|
545
545
|
export declare function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
546
546
|
getBotToken: () => string | undefined;
|
|
547
547
|
getBotScope?: () => string | undefined;
|
|
548
|
+
isThreadedMode?: () => boolean;
|
|
548
549
|
recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
|
|
549
550
|
captureRequestErrorHandler?: TelegramBridgeApiRuntimeDeps["captureRequestErrorHandler"];
|
|
550
551
|
targetActivity?: TelegramApiTargetActivityRuntime;
|
|
@@ -560,5 +561,6 @@ export declare function createTelegramBridgeApiRuntime(deps: TelegramBridgeApiRu
|
|
|
560
561
|
*/
|
|
561
562
|
export declare function createTelegramApiClient(getBotToken: () => string | undefined, options?: TelegramAnswerCallbackQueryOptions & {
|
|
562
563
|
now?: () => number;
|
|
564
|
+
isThreadedMode?: () => boolean;
|
|
563
565
|
}): TelegramApiClient;
|
|
564
566
|
export {};
|