@llblab/pi-telegram 0.54.4 → 0.55.1
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 +16 -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 +3 -1
- package/dist/lib/bindings.js +8 -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 +50 -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 +7 -3
- package/dist/lib/bus.js +24 -16
- package/dist/lib/delivery.d.ts +4 -0
- package/dist/lib/delivery.js +48 -15
- package/dist/lib/extension.js +48 -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 +7 -2
- package/dist/lib/routing.js +585 -229
- 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 +18 -5
- package/dist/lib/threads.js +107 -30
- package/dist/lib/turns.d.ts +6 -0
- package/dist/lib/turns.js +12 -1
- package/dist/lib/updates.d.ts +13 -4
- package/dist/lib/updates.js +54 -19
- package/dist/package.json +1 -1
- package/docs/activity.md +1 -1
- package/docs/architecture.md +14 -11
- package/docs/delivery.md +7 -5
- package/docs/multi-instance-bus.md +20 -8
- package/docs/public-api.md +2 -2
- package/docs/ui-style.md +2 -2
- package/lib/activity.ts +29 -5
- package/lib/bindings.ts +9 -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 +30 -18
- package/lib/delivery.ts +47 -19
- package/lib/extension.ts +51 -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 +614 -257
- 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 +120 -36
- package/lib/turns.ts +15 -1
- package/lib/updates.ts +60 -21
- package/package.json +1 -1
package/dist/lib/extension.js
CHANGED
|
@@ -65,6 +65,8 @@ const telegramBusProtocolIdentity = Bus.createTelegramCurrentBusProtocolIdentity
|
|
|
65
65
|
Bus.TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
66
66
|
Bus.TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
67
67
|
Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
68
|
+
Bus.TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND,
|
|
69
|
+
Bus.TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION,
|
|
68
70
|
Bus.TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
|
|
69
71
|
Bus.TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
70
72
|
Bus.TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
@@ -303,7 +305,6 @@ export default function (pi) {
|
|
|
303
305
|
const proactivePushChatIdGetter = Config.createTelegramProactivePushChatIdGetter(proactivePushTargetGetter);
|
|
304
306
|
const buttonActionStore = Outbound.createTelegramButtonActionStore();
|
|
305
307
|
const planGenerativeAppOutput = Outbound.createTelegramOutboundReplyPlanner(buttonActionStore, configControls.getAssistantRenderingMode);
|
|
306
|
-
const pendingModelSwitchStore = Model.createPendingModelSwitchStore();
|
|
307
308
|
const modelMenuRuntime = Menu.createTelegramModelMenuRuntime();
|
|
308
309
|
const sectionRegistry = Sections.createAndBindTelegramSectionRegistry();
|
|
309
310
|
const timeInjectionRuntime = TimeInjection.createTimeInjectionRuntime({
|
|
@@ -323,7 +324,24 @@ export default function (pi) {
|
|
|
323
324
|
getBotToken: configStore.getBotToken,
|
|
324
325
|
});
|
|
325
326
|
const telegramQueueStore = Queue.createTelegramTransportStampedQueueStore(rawTelegramQueueStore, telegramTransportStampRuntime.getStamp);
|
|
326
|
-
const
|
|
327
|
+
const captureCleanupDrain = Lifecycle.createTelegramSessionDrainObserver({
|
|
328
|
+
context: telegramSessionContextStore,
|
|
329
|
+
captureAuthority() {
|
|
330
|
+
const epoch = getCurrentLeaderEpoch();
|
|
331
|
+
return epoch === undefined ? undefined : {
|
|
332
|
+
epoch, operator: configStore.getAllowedUserId(), stamp: telegramTransportStampRuntime.getStamp(),
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
isAuthorityActive(authority) {
|
|
336
|
+
return getCurrentLeaderEpoch() === authority.epoch &&
|
|
337
|
+
configStore.getAllowedUserId() === authority.operator &&
|
|
338
|
+
telegramTransportStampRuntime.isActive(authority.stamp);
|
|
339
|
+
},
|
|
340
|
+
onDrained(ctx) { inboundRouteRuntime.onDeliverySettled(ctx); },
|
|
341
|
+
});
|
|
342
|
+
const telegramApiTargetActivityRuntime = TelegramApi.createTelegramApiTargetActivityRuntime({
|
|
343
|
+
captureDrain: captureCleanupDrain,
|
|
344
|
+
});
|
|
327
345
|
// Retirement protection and dead-owner reclamation must inspect the same journal sources.
|
|
328
346
|
const workspaceRetirementJournalPorts = {
|
|
329
347
|
getActiveTurnTarget: activeTurnRuntime.getTarget,
|
|
@@ -533,7 +551,10 @@ export default function (pi) {
|
|
|
533
551
|
hasDispatchPending: lifecycle.hasDispatchPending,
|
|
534
552
|
isCompactionInProgress: lifecycle.isCompactionInProgress,
|
|
535
553
|
getActiveToolExecutions: lifecycle.getActiveToolExecutions,
|
|
536
|
-
|
|
554
|
+
// Status is composed before the model-switch controller; it reads the controller's state when it renders.
|
|
555
|
+
hasPendingModelSwitch() {
|
|
556
|
+
return modelSwitchController.hasPendingSwitch();
|
|
557
|
+
},
|
|
537
558
|
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
538
559
|
getQueuedItemCount: Queue.countExecutableTelegramQueueItems,
|
|
539
560
|
formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
|
|
@@ -573,6 +594,11 @@ export default function (pi) {
|
|
|
573
594
|
// --- Telegram API ---
|
|
574
595
|
const directTelegramApiRuntime = TelegramApi.createDefaultTelegramBridgeApiRuntime({
|
|
575
596
|
getBotToken: configStore.getBotToken,
|
|
597
|
+
isThreadedMode() {
|
|
598
|
+
return threadStore.getBotState().threadMode === "enabled" ||
|
|
599
|
+
telegramBusFollowerRegistrationState.getTarget()?.threadId !== undefined ||
|
|
600
|
+
telegramBusLeaderState.getTarget()?.threadId !== undefined;
|
|
601
|
+
},
|
|
576
602
|
getBotScope() {
|
|
577
603
|
const config = configStore.get();
|
|
578
604
|
return (config.botUsername ??
|
|
@@ -631,9 +657,13 @@ export default function (pi) {
|
|
|
631
657
|
lifecycle,
|
|
632
658
|
typing,
|
|
633
659
|
getDefaultChatId: proactivePushChatIdGetter,
|
|
660
|
+
isThreadedMode() {
|
|
661
|
+
return deliveryTargetPolicyRuntime.getTargetPolicyView().threaded === true;
|
|
662
|
+
},
|
|
634
663
|
sendTypingAction,
|
|
635
664
|
updateStatus,
|
|
636
665
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
666
|
+
getSessionGeneration: telegramSessionContextStore.getGeneration,
|
|
637
667
|
getTransportAuthority() {
|
|
638
668
|
if (ownsTelegramDirectDelivery()) {
|
|
639
669
|
const epoch = getCurrentLeaderEpoch();
|
|
@@ -661,6 +691,11 @@ export default function (pi) {
|
|
|
661
691
|
const { replyTransport, editInteractiveMessage, sendInteractiveMessage, sendSectionRichMessage, } = replyRuntime;
|
|
662
692
|
const deliveryTargetPolicyRuntime = Delivery.createTelegramDeliveryTargetPolicyRuntime({
|
|
663
693
|
ownsDirect: lockRuntime.owns,
|
|
694
|
+
isThreadedMode() {
|
|
695
|
+
return threadStore.getBotState().threadMode === "enabled" ||
|
|
696
|
+
telegramBusFollowerRegistrationState.getTarget()?.threadId !== undefined ||
|
|
697
|
+
telegramBusLeaderState.getTarget()?.threadId !== undefined;
|
|
698
|
+
},
|
|
664
699
|
isFollowerRegistered: telegramBusFollowerRegistrationState.isRegistered,
|
|
665
700
|
getAllowedChatId: configStore.getAllowedUserId,
|
|
666
701
|
getFollowerTarget: telegramBusFollowerRegistrationState.getTarget,
|
|
@@ -672,6 +707,7 @@ export default function (pi) {
|
|
|
672
707
|
const deliveryGenerationSeed = Delivery.createTelegramDeliveryGenerationSeed(telegramInstanceId);
|
|
673
708
|
const deliveryLifecycleRuntime = Delivery.createTelegramBridgeDeliveryLifecycleHooks({
|
|
674
709
|
generationSeed: deliveryGenerationSeed,
|
|
710
|
+
captureDrain: captureCleanupDrain,
|
|
675
711
|
getTargetPolicyView: deliveryTargetPolicyRuntime.getTargetPolicyView,
|
|
676
712
|
getTransportStamp: telegramTransportStampRuntime.getStamp,
|
|
677
713
|
isTransportStampActive: telegramTransportStampRuntime.isActive,
|
|
@@ -723,6 +759,7 @@ export default function (pi) {
|
|
|
723
759
|
});
|
|
724
760
|
const { activityRuntime, activityVerbosityRuntime, assistantOutputRuntime, publicationRuntime, } = Bindings.createTelegramActivityBindingRuntime({
|
|
725
761
|
generation: deliveryGenerationSeed,
|
|
762
|
+
captureDrain: captureCleanupDrain,
|
|
726
763
|
assistantOutput: {
|
|
727
764
|
prepareTelegramPreview: previewRuntime.preparePublication,
|
|
728
765
|
authority: {
|
|
@@ -788,8 +825,6 @@ export default function (pi) {
|
|
|
788
825
|
// --- Model And Menu Setup ---
|
|
789
826
|
const modelSwitchController = Model.createTelegramModelSwitchControllerRuntime({
|
|
790
827
|
isIdle,
|
|
791
|
-
getPendingModelSwitch: pendingModelSwitchStore.get,
|
|
792
|
-
setPendingModelSwitch: pendingModelSwitchStore.set,
|
|
793
828
|
getActiveTurn: activeTurnRuntime.get,
|
|
794
829
|
getAbortHandler: abort.getHandler,
|
|
795
830
|
hasAbortHandler: abort.hasHandler,
|
|
@@ -987,7 +1022,7 @@ export default function (pi) {
|
|
|
987
1022
|
lifecycle: bridgeRuntime.lifecycle,
|
|
988
1023
|
isIdle,
|
|
989
1024
|
hasPendingMessages,
|
|
990
|
-
hasPendingControl:
|
|
1025
|
+
hasPendingControl: modelSwitchController.hasPendingSwitch,
|
|
991
1026
|
publication: publicationRuntime,
|
|
992
1027
|
activity: activityRuntime,
|
|
993
1028
|
delivery: deliveryLifecycleRuntime,
|
|
@@ -1125,6 +1160,11 @@ export default function (pi) {
|
|
|
1125
1160
|
return (lockRuntime.owns() &&
|
|
1126
1161
|
Bus.hasTelegramBusCapability(telegramBusProtocolIdentity, Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE));
|
|
1127
1162
|
},
|
|
1163
|
+
canUsePreDispatchRebind(recipient) {
|
|
1164
|
+
return lockRuntime.owns() && telegramBusFollowerRegistry.list().every(function (follower) {
|
|
1165
|
+
return Bus.hasTelegramBusCapability(follower.protocol, Bus.TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND);
|
|
1166
|
+
}) && (recipient?.kind !== "follower" || Bus.hasTelegramBusCapability(telegramBusFollowerRegistry.get(recipient.instanceId)?.protocol, Bus.TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION));
|
|
1167
|
+
},
|
|
1128
1168
|
hasWorkspaceLiveRebindAuthority() {
|
|
1129
1169
|
return (lockRuntime.owns() &&
|
|
1130
1170
|
Bus.hasTelegramBusCapability(telegramBusProtocolIdentity, Bus.TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE) &&
|
|
@@ -1739,6 +1779,7 @@ export default function (pi) {
|
|
|
1739
1779
|
followerRegistry: telegramBusFollowerRegistry,
|
|
1740
1780
|
authSecret: telegramBusAuthSecret,
|
|
1741
1781
|
protocolIdentity: telegramBusProtocolIdentity,
|
|
1782
|
+
requiresPreDispatchRebindReader() { return threadStore.hasPreDispatchRebindEvidence(); },
|
|
1742
1783
|
selectedMenuDelivery,
|
|
1743
1784
|
startPolling: pollingAdmissionRuntime.start,
|
|
1744
1785
|
stopPolling: pollingAdmissionRuntime.stop,
|
|
@@ -2442,6 +2483,7 @@ export default function (pi) {
|
|
|
2442
2483
|
isSessionContextActive(ctx) {
|
|
2443
2484
|
return telegramSessionContextStore.isCurrent(ctx);
|
|
2444
2485
|
},
|
|
2486
|
+
getSessionGeneration: telegramSessionContextStore.getGeneration,
|
|
2445
2487
|
isTurnTransportActive(turn) {
|
|
2446
2488
|
return telegramTransportStampRuntime.isActive(turn.transportStamp);
|
|
2447
2489
|
},
|
package/dist/lib/lifecycle.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import * as BusFollower from "./bus-follower.ts";
|
|
8
8
|
import type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AssistantMessageEvent, BeforeAgentStartEvent, ExtensionAPI, ExtensionContext, InputEvent, MessageEndEvent, SessionBeforeCompactEvent, SessionCompactEvent, SessionCompactFailedEvent, SessionShutdownEvent, SessionStartEvent, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, UIPromptEndEvent, UIPromptStartEvent } from "./pi.ts";
|
|
9
|
+
import * as Machine from "./machine.ts";
|
|
9
10
|
import * as Queue from "./queue.ts";
|
|
10
11
|
export declare function createAgentStartDedupHook(inner: (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>, resetReplyDedup: () => void, schedulePublication?: (task: () => Promise<void>) => void): (event: AgentStartEvent, ctx: ExtensionContext) => Promise<void>;
|
|
11
12
|
type TelegramBeforeAgentStartEvent = Omit<BeforeAgentStartEvent, "systemPrompt"> & {
|
|
@@ -112,6 +113,13 @@ export interface TelegramSessionContextStore<TContext> {
|
|
|
112
113
|
export declare function createTelegramSessionContextStore<TContext>(options?: {
|
|
113
114
|
getIdentity?: (ctx: TContext) => unknown;
|
|
114
115
|
}): TelegramSessionContextStore<TContext>;
|
|
116
|
+
/** Capture drain hints at work admission; late completion never borrows a replacement session or transport. */
|
|
117
|
+
export declare function createTelegramSessionDrainObserver<TContext, TAuthority>(deps: {
|
|
118
|
+
context: Pick<TelegramSessionContextStore<TContext>, "get" | "getGeneration" | "isCurrent">;
|
|
119
|
+
captureAuthority: () => TAuthority | undefined;
|
|
120
|
+
isAuthorityActive: (authority: TAuthority) => boolean;
|
|
121
|
+
onDrained: (ctx: TContext) => void;
|
|
122
|
+
}): () => (() => void) | undefined;
|
|
115
123
|
export declare function createTelegramSessionGenerationFence(store: TelegramSessionContextStore<ExtensionContext>, hooks: TelegramSessionLifecycleHooks): TelegramSessionLifecycleHooks;
|
|
116
124
|
export interface TelegramBridgeSessionServiceRuntime {
|
|
117
125
|
connection?: TelegramConnectionLifecycle;
|
|
@@ -192,9 +200,69 @@ export interface TelegramCompactionObserverRuntime<TContext> {
|
|
|
192
200
|
onSessionCompactFailed: (event: SessionCompactFailedEvent, ctx: TContext) => void;
|
|
193
201
|
onSessionShutdown: () => void;
|
|
194
202
|
}
|
|
203
|
+
/** The whole compaction-observer state; only `transitionTelegramCompactionObserver` produces a new one. */
|
|
204
|
+
export interface TelegramCompactionObserverState {
|
|
205
|
+
/** The fallback timer's observation; a timer from any other one is inert. */
|
|
206
|
+
readonly observation: number | null;
|
|
207
|
+
/** The typing loop is the observer's to stop; a loop the agent already ran is never stopped by compaction. */
|
|
208
|
+
readonly typingOwned: boolean;
|
|
209
|
+
readonly nextObservation: number;
|
|
210
|
+
}
|
|
211
|
+
export type TelegramCompactionObserverEvent = {
|
|
212
|
+
type: "beforeCompact";
|
|
213
|
+
}
|
|
214
|
+
/** The outcome of the observer's own typing start, reported back by the shell. */
|
|
215
|
+
| {
|
|
216
|
+
type: "typingStarted";
|
|
217
|
+
started: boolean;
|
|
218
|
+
} | {
|
|
219
|
+
type: "compacted";
|
|
220
|
+
} | {
|
|
221
|
+
type: "compactFailed";
|
|
222
|
+
} | {
|
|
223
|
+
type: "timerFired";
|
|
224
|
+
observation: number;
|
|
225
|
+
contextActive: boolean;
|
|
226
|
+
} | {
|
|
227
|
+
type: "shutdown";
|
|
228
|
+
};
|
|
229
|
+
export type TelegramCompactionObserverEffect = {
|
|
230
|
+
type: "setInProgress";
|
|
231
|
+
inProgress: boolean;
|
|
232
|
+
} | {
|
|
233
|
+
type: "startTyping";
|
|
234
|
+
} | {
|
|
235
|
+
type: "stopTyping";
|
|
236
|
+
} | {
|
|
237
|
+
type: "updateStatus";
|
|
238
|
+
} | {
|
|
239
|
+
type: "armTimer";
|
|
240
|
+
observation: number;
|
|
241
|
+
} | {
|
|
242
|
+
type: "cancelTimer";
|
|
243
|
+
} | {
|
|
244
|
+
type: "recordTimeout";
|
|
245
|
+
} | {
|
|
246
|
+
type: "abandoned";
|
|
247
|
+
} | {
|
|
248
|
+
type: "requestDispatch";
|
|
249
|
+
};
|
|
250
|
+
export declare function createTelegramCompactionObserverState(): TelegramCompactionObserverState;
|
|
251
|
+
/**
|
|
252
|
+
* Pure compaction-observer transition. A completion or failure always releases presence; the timeout releases only its
|
|
253
|
+
* own observation. Typing ownership is sticky: a repeated start while the observer's own loop runs reports `false`
|
|
254
|
+
* from the shared loop, and must not hand that loop to nobody.
|
|
255
|
+
*/
|
|
256
|
+
export declare function transitionTelegramCompactionObserver(state: TelegramCompactionObserverState, event: TelegramCompactionObserverEvent): Machine.TelegramMachineStep<TelegramCompactionObserverState, TelegramCompactionObserverEffect>;
|
|
257
|
+
/**
|
|
258
|
+
* The shell around `transitionTelegramCompactionObserver`: it fences inactive contexts, runs effects with the calling
|
|
259
|
+
* context, reports the typing start back and owns the fallback timer, which keeps the context it was armed under.
|
|
260
|
+
*/
|
|
195
261
|
export declare function createTelegramCompactionObserverRuntime<TContext>(deps: TelegramCompactionObserverRuntimeDeps<TContext>): TelegramCompactionObserverRuntime<TContext>;
|
|
196
262
|
export interface TelegramMessageActivityTypingDeps<TContext> {
|
|
197
263
|
hasActiveTurn: () => boolean;
|
|
264
|
+
isContextActive?: (ctx: TContext) => boolean;
|
|
265
|
+
getSessionGeneration?: () => number;
|
|
198
266
|
startTypingLoop: (ctx: TContext) => void;
|
|
199
267
|
onMessageStart: TelegramLifecycleRegistrationDeps["onMessageStart"];
|
|
200
268
|
onMessageUpdate: TelegramLifecycleRegistrationDeps["onMessageUpdate"];
|
package/dist/lib/lifecycle.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import * as BusFollower from "./bus-follower.js";
|
|
9
|
+
import * as Machine from "./machine.js";
|
|
9
10
|
import * as Queue from "./queue.js";
|
|
10
11
|
import { formatTelegramConnectionFailure } from "./status.js";
|
|
11
12
|
import * as TextGroups from "./text-groups.js";
|
|
@@ -195,6 +196,31 @@ export function createTelegramSessionContextStore(options = {}) {
|
|
|
195
196
|
},
|
|
196
197
|
};
|
|
197
198
|
}
|
|
199
|
+
/** Capture drain hints at work admission; late completion never borrows a replacement session or transport. */
|
|
200
|
+
export function createTelegramSessionDrainObserver(deps) {
|
|
201
|
+
return () => {
|
|
202
|
+
try {
|
|
203
|
+
const ctx = deps.context.get(), generation = deps.context.getGeneration(), authority = deps.captureAuthority();
|
|
204
|
+
if (ctx === undefined || authority === undefined)
|
|
205
|
+
return undefined;
|
|
206
|
+
return () => {
|
|
207
|
+
queueMicrotask(() => {
|
|
208
|
+
try {
|
|
209
|
+
if (!deps.context.isCurrent(ctx, generation) || !deps.isAuthorityActive(authority))
|
|
210
|
+
return;
|
|
211
|
+
const emitted = deps.onDrained(ctx);
|
|
212
|
+
if (emitted)
|
|
213
|
+
void Promise.resolve(emitted).catch(() => undefined);
|
|
214
|
+
}
|
|
215
|
+
catch { /* A drain hint is observational, not execution authority. */ }
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
}
|
|
198
224
|
export function createTelegramSessionGenerationFence(store, hooks) {
|
|
199
225
|
return {
|
|
200
226
|
async onSessionStart(event, ctx) {
|
|
@@ -329,76 +355,165 @@ function unrefTelegramLifecycleTimer(timer) {
|
|
|
329
355
|
if (typeof timer.unref === "function")
|
|
330
356
|
timer.unref();
|
|
331
357
|
}
|
|
358
|
+
export function createTelegramCompactionObserverState() {
|
|
359
|
+
return { observation: null, typingOwned: false, nextObservation: 0 };
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Pure compaction-observer transition. A completion or failure always releases presence; the timeout releases only its
|
|
363
|
+
* own observation. Typing ownership is sticky: a repeated start while the observer's own loop runs reports `false`
|
|
364
|
+
* from the shared loop, and must not hand that loop to nobody.
|
|
365
|
+
*/
|
|
366
|
+
export function transitionTelegramCompactionObserver(state, event) {
|
|
367
|
+
const cancel = state.observation === null ? [] : [{ type: "cancelTimer" }];
|
|
368
|
+
const release = (...tail) => [
|
|
369
|
+
{ type: "setInProgress", inProgress: false },
|
|
370
|
+
...(state.typingOwned ? [{ type: "stopTyping" }] : []),
|
|
371
|
+
{ type: "updateStatus" },
|
|
372
|
+
...tail,
|
|
373
|
+
];
|
|
374
|
+
switch (event.type) {
|
|
375
|
+
case "beforeCompact": {
|
|
376
|
+
const observation = state.nextObservation + 1;
|
|
377
|
+
return {
|
|
378
|
+
state: { ...state, observation, nextObservation: observation },
|
|
379
|
+
effects: [
|
|
380
|
+
{ type: "setInProgress", inProgress: true },
|
|
381
|
+
{ type: "startTyping" },
|
|
382
|
+
{ type: "updateStatus" },
|
|
383
|
+
...cancel,
|
|
384
|
+
{ type: "armTimer", observation },
|
|
385
|
+
],
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
case "typingStarted":
|
|
389
|
+
return {
|
|
390
|
+
state: { ...state, typingOwned: state.typingOwned || event.started },
|
|
391
|
+
effects: [],
|
|
392
|
+
};
|
|
393
|
+
case "compacted":
|
|
394
|
+
return {
|
|
395
|
+
state: { ...state, observation: null, typingOwned: false },
|
|
396
|
+
effects: [...cancel, ...release({ type: "requestDispatch" })],
|
|
397
|
+
};
|
|
398
|
+
case "compactFailed":
|
|
399
|
+
return {
|
|
400
|
+
state: { ...state, observation: null, typingOwned: false },
|
|
401
|
+
effects: [
|
|
402
|
+
...cancel,
|
|
403
|
+
...release({ type: "abandoned" }, { type: "requestDispatch" }),
|
|
404
|
+
],
|
|
405
|
+
};
|
|
406
|
+
case "timerFired":
|
|
407
|
+
if (state.observation !== event.observation)
|
|
408
|
+
return { state, effects: [] };
|
|
409
|
+
// A replaced context consumes its observation without releasing the replacement's presence.
|
|
410
|
+
if (!event.contextActive)
|
|
411
|
+
return { state: { ...state, observation: null }, effects: [] };
|
|
412
|
+
return {
|
|
413
|
+
state: { ...state, observation: null, typingOwned: false },
|
|
414
|
+
effects: release({ type: "recordTimeout" }, { type: "requestDispatch" }),
|
|
415
|
+
};
|
|
416
|
+
case "shutdown":
|
|
417
|
+
return {
|
|
418
|
+
state: { ...state, observation: null, typingOwned: false },
|
|
419
|
+
effects: [
|
|
420
|
+
...cancel,
|
|
421
|
+
...(state.typingOwned ? [{ type: "stopTyping" }] : []),
|
|
422
|
+
],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* The shell around `transitionTelegramCompactionObserver`: it fences inactive contexts, runs effects with the calling
|
|
428
|
+
* context, reports the typing start back and owns the fallback timer, which keeps the context it was armed under.
|
|
429
|
+
*/
|
|
332
430
|
export function createTelegramCompactionObserverRuntime(deps) {
|
|
333
431
|
const timeoutMs = deps.timeoutMs ?? 300_000;
|
|
334
432
|
const setTimer = deps.setTimer ?? setTimeout;
|
|
335
433
|
const clearTimer = deps.clearTimer ?? clearTimeout;
|
|
336
434
|
let fallbackTimer;
|
|
337
|
-
|
|
338
|
-
let
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
435
|
+
// The context of the event being dispatched; effects run synchronously inside that dispatch.
|
|
436
|
+
let call;
|
|
437
|
+
const machine = Machine.createTelegramMachineRunner({
|
|
438
|
+
initial: createTelegramCompactionObserverState(),
|
|
439
|
+
transition: transitionTelegramCompactionObserver,
|
|
440
|
+
run(effect) {
|
|
441
|
+
const ctx = call;
|
|
442
|
+
switch (effect.type) {
|
|
443
|
+
case "setInProgress":
|
|
444
|
+
deps.setCompactionInProgress(effect.inProgress);
|
|
445
|
+
return;
|
|
446
|
+
case "startTyping": {
|
|
447
|
+
const result = deps.startTypingLoop?.(ctx);
|
|
448
|
+
machine.dispatch({
|
|
449
|
+
type: "typingStarted",
|
|
450
|
+
started: !!deps.startTypingLoop && result !== false,
|
|
451
|
+
});
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
case "stopTyping":
|
|
455
|
+
deps.stopTypingLoop?.();
|
|
456
|
+
return;
|
|
457
|
+
case "updateStatus":
|
|
458
|
+
deps.updateStatus(ctx);
|
|
459
|
+
return;
|
|
460
|
+
case "armTimer": {
|
|
461
|
+
const armedCtx = ctx;
|
|
462
|
+
fallbackTimer = setTimer(() => {
|
|
463
|
+
fallbackTimer = undefined;
|
|
464
|
+
dispatchFor(armedCtx, {
|
|
465
|
+
type: "timerFired",
|
|
466
|
+
observation: effect.observation,
|
|
467
|
+
contextActive: !deps.isContextActive || deps.isContextActive(armedCtx),
|
|
468
|
+
});
|
|
469
|
+
}, timeoutMs);
|
|
470
|
+
unrefTelegramLifecycleTimer(fallbackTimer);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
case "cancelTimer":
|
|
474
|
+
if (fallbackTimer !== undefined)
|
|
475
|
+
clearTimer(fallbackTimer);
|
|
476
|
+
fallbackTimer = undefined;
|
|
477
|
+
return;
|
|
478
|
+
case "recordTimeout":
|
|
479
|
+
deps.recordRuntimeEvent?.("compact", new Error("Compaction observer timed out"));
|
|
480
|
+
return;
|
|
481
|
+
case "abandoned":
|
|
482
|
+
deps.onCompactionAbandoned?.();
|
|
483
|
+
return;
|
|
484
|
+
case "requestDispatch":
|
|
485
|
+
// Observer expiry releases local presence, not Pi's eventual terminal result.
|
|
486
|
+
deps.requestDeferredDispatchNextQueuedTelegramTurn(deps.dispatchNextQueuedTelegramTurn);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
const dispatchFor = (ctx, event) => {
|
|
492
|
+
const previous = call;
|
|
493
|
+
call = ctx;
|
|
494
|
+
try {
|
|
495
|
+
machine.dispatch(event);
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
call = previous;
|
|
499
|
+
}
|
|
356
500
|
};
|
|
501
|
+
const isActive = (ctx) => !deps.isContextActive || deps.isContextActive(ctx);
|
|
357
502
|
return {
|
|
358
503
|
onSessionBeforeCompact: (_event, ctx) => {
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
deps.setCompactionInProgress(true);
|
|
362
|
-
const typingStartResult = deps.startTypingLoop?.(ctx);
|
|
363
|
-
typingStartedByObserver =
|
|
364
|
-
!!deps.startTypingLoop && typingStartResult !== false;
|
|
365
|
-
deps.updateStatus(ctx);
|
|
366
|
-
clearFallbackTimer();
|
|
367
|
-
const admittedGeneration = observationGeneration;
|
|
368
|
-
fallbackTimer = setTimer(() => {
|
|
369
|
-
if (observationGeneration !== admittedGeneration)
|
|
370
|
-
return;
|
|
371
|
-
observationGeneration += 1;
|
|
372
|
-
fallbackTimer = undefined;
|
|
373
|
-
if (deps.isContextActive && !deps.isContextActive(ctx))
|
|
374
|
-
return;
|
|
375
|
-
releaseCompactionPresence(ctx);
|
|
376
|
-
deps.recordRuntimeEvent?.("compact", new Error("Compaction observer timed out"));
|
|
377
|
-
// Observer expiry releases local presence, not Pi's eventual terminal result.
|
|
378
|
-
requestDispatch();
|
|
379
|
-
}, timeoutMs);
|
|
380
|
-
unrefTelegramLifecycleTimer(fallbackTimer);
|
|
504
|
+
if (isActive(ctx))
|
|
505
|
+
dispatchFor(ctx, { type: "beforeCompact" });
|
|
381
506
|
},
|
|
382
507
|
onSessionCompact: (_event, ctx) => {
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
clearFallbackTimer();
|
|
386
|
-
releaseCompactionPresence(ctx);
|
|
387
|
-
requestDispatch();
|
|
508
|
+
if (isActive(ctx))
|
|
509
|
+
dispatchFor(ctx, { type: "compacted" });
|
|
388
510
|
},
|
|
389
511
|
onSessionCompactFailed: (_event, ctx) => {
|
|
390
|
-
if (
|
|
391
|
-
|
|
392
|
-
clearFallbackTimer();
|
|
393
|
-
releaseCompactionPresence(ctx);
|
|
394
|
-
deps.onCompactionAbandoned?.();
|
|
395
|
-
requestDispatch();
|
|
512
|
+
if (isActive(ctx))
|
|
513
|
+
dispatchFor(ctx, { type: "compactFailed" });
|
|
396
514
|
},
|
|
397
515
|
onSessionShutdown: () => {
|
|
398
|
-
|
|
399
|
-
if (typingStartedByObserver)
|
|
400
|
-
deps.stopTypingLoop?.();
|
|
401
|
-
typingStartedByObserver = false;
|
|
516
|
+
dispatchFor(undefined, { type: "shutdown" });
|
|
402
517
|
},
|
|
403
518
|
};
|
|
404
519
|
}
|
|
@@ -409,7 +524,11 @@ export function createTelegramMessageActivityTypingHooks(deps) {
|
|
|
409
524
|
};
|
|
410
525
|
const handleMessageActivity = async (phase, event, ctx, inner) => {
|
|
411
526
|
const typedCtx = ctx;
|
|
412
|
-
|
|
527
|
+
const generation = deps.getSessionGeneration?.();
|
|
528
|
+
const isCurrent = () => deps.isContextActive?.(typedCtx) !== false &&
|
|
529
|
+
(!deps.getSessionGeneration || Object.is(deps.getSessionGeneration(), generation));
|
|
530
|
+
if (isCurrent())
|
|
531
|
+
ensureTyping(typedCtx);
|
|
413
532
|
try {
|
|
414
533
|
await inner(event, ctx);
|
|
415
534
|
}
|
|
@@ -417,7 +536,8 @@ export function createTelegramMessageActivityTypingHooks(deps) {
|
|
|
417
536
|
deps.recordRuntimeEvent?.("message-activity", error, { phase });
|
|
418
537
|
}
|
|
419
538
|
finally {
|
|
420
|
-
|
|
539
|
+
if (isCurrent())
|
|
540
|
+
ensureTyping(typedCtx);
|
|
421
541
|
}
|
|
422
542
|
};
|
|
423
543
|
return {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure state-machine runner
|
|
3
|
+
* Zones: shared utils
|
|
4
|
+
* Owns committing a pure transition's next state, running the effects it returns and waking state waiters
|
|
5
|
+
* Knows nothing about the machines it drives: their state, events, effects, timers and transports stay with each domain
|
|
6
|
+
*/
|
|
7
|
+
/** One transition result: the next state plus effects as data. A machine may add fields, such as a call result. */
|
|
8
|
+
export interface TelegramMachineStep<State, Effect> {
|
|
9
|
+
state: State;
|
|
10
|
+
effects: readonly Effect[];
|
|
11
|
+
}
|
|
12
|
+
export interface TelegramMachineRunner<State, Event, Step> {
|
|
13
|
+
readonly state: State;
|
|
14
|
+
/** Commits the next state, runs its effects (which may dispatch again), wakes waiters and returns the step. */
|
|
15
|
+
dispatch(event: Event): Step;
|
|
16
|
+
/** Resolves once `ready` holds for the committed state, checking now and after every later dispatch. */
|
|
17
|
+
waitFor(ready: (state: State) => boolean): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export declare function createTelegramMachineRunner<State, Event, Effect, Step extends TelegramMachineStep<State, Effect> = TelegramMachineStep<State, Effect>>(options: {
|
|
20
|
+
initial: State;
|
|
21
|
+
transition: (state: State, event: Event) => Step;
|
|
22
|
+
run: (effect: Effect) => void;
|
|
23
|
+
}): TelegramMachineRunner<State, Event, Step>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure state-machine runner
|
|
3
|
+
* Zones: shared utils
|
|
4
|
+
* Owns committing a pure transition's next state, running the effects it returns and waking state waiters
|
|
5
|
+
* Knows nothing about the machines it drives: their state, events, effects, timers and transports stay with each domain
|
|
6
|
+
*/
|
|
7
|
+
export function createTelegramMachineRunner(options) {
|
|
8
|
+
let state = options.initial;
|
|
9
|
+
const waiters = new Set();
|
|
10
|
+
return {
|
|
11
|
+
get state() {
|
|
12
|
+
return state;
|
|
13
|
+
},
|
|
14
|
+
dispatch(event) {
|
|
15
|
+
const next = options.transition(state, event);
|
|
16
|
+
state = next.state;
|
|
17
|
+
for (const effect of next.effects)
|
|
18
|
+
options.run(effect);
|
|
19
|
+
for (const waiter of [...waiters])
|
|
20
|
+
waiter();
|
|
21
|
+
return next;
|
|
22
|
+
},
|
|
23
|
+
waitFor(ready) {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
if (ready(state))
|
|
26
|
+
return resolve();
|
|
27
|
+
const waiter = () => {
|
|
28
|
+
if (!ready(state))
|
|
29
|
+
return;
|
|
30
|
+
waiters.delete(waiter);
|
|
31
|
+
resolve();
|
|
32
|
+
};
|
|
33
|
+
waiters.add(waiter);
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|