@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/lib/extension.ts
CHANGED
|
@@ -70,6 +70,8 @@ const telegramBusProtocolIdentity =
|
|
|
70
70
|
Bus.TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
71
71
|
Bus.TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
72
72
|
Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
73
|
+
Bus.TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND,
|
|
74
|
+
Bus.TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION,
|
|
73
75
|
Bus.TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
|
|
74
76
|
Bus.TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
75
77
|
Bus.TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
@@ -389,10 +391,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
389
391
|
buttonActionStore,
|
|
390
392
|
configControls.getAssistantRenderingMode,
|
|
391
393
|
);
|
|
392
|
-
const pendingModelSwitchStore =
|
|
393
|
-
Model.createPendingModelSwitchStore<
|
|
394
|
-
Model.ScopedTelegramModel<ActivePiModel>
|
|
395
|
-
>();
|
|
396
394
|
const modelMenuRuntime = Menu.createTelegramModelMenuRuntime<ActivePiModel>();
|
|
397
395
|
const sectionRegistry = Sections.createAndBindTelegramSectionRegistry();
|
|
398
396
|
|
|
@@ -424,8 +422,25 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
424
422
|
rawTelegramQueueStore,
|
|
425
423
|
telegramTransportStampRuntime.getStamp,
|
|
426
424
|
);
|
|
425
|
+
const captureCleanupDrain = Lifecycle.createTelegramSessionDrainObserver({
|
|
426
|
+
context: telegramSessionContextStore,
|
|
427
|
+
captureAuthority() {
|
|
428
|
+
const epoch = getCurrentLeaderEpoch();
|
|
429
|
+
return epoch === undefined ? undefined : {
|
|
430
|
+
epoch, operator: configStore.getAllowedUserId(), stamp: telegramTransportStampRuntime.getStamp(),
|
|
431
|
+
};
|
|
432
|
+
},
|
|
433
|
+
isAuthorityActive(authority) {
|
|
434
|
+
return getCurrentLeaderEpoch() === authority.epoch &&
|
|
435
|
+
configStore.getAllowedUserId() === authority.operator &&
|
|
436
|
+
telegramTransportStampRuntime.isActive(authority.stamp);
|
|
437
|
+
},
|
|
438
|
+
onDrained(ctx) { inboundRouteRuntime.onDeliverySettled(ctx); },
|
|
439
|
+
});
|
|
427
440
|
const telegramApiTargetActivityRuntime =
|
|
428
|
-
TelegramApi.createTelegramApiTargetActivityRuntime(
|
|
441
|
+
TelegramApi.createTelegramApiTargetActivityRuntime({
|
|
442
|
+
captureDrain: captureCleanupDrain,
|
|
443
|
+
});
|
|
429
444
|
// Retirement protection and dead-owner reclamation must inspect the same journal sources.
|
|
430
445
|
const workspaceRetirementJournalPorts = {
|
|
431
446
|
getActiveTurnTarget: activeTurnRuntime.getTarget,
|
|
@@ -696,7 +711,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
696
711
|
hasDispatchPending: lifecycle.hasDispatchPending,
|
|
697
712
|
isCompactionInProgress: lifecycle.isCompactionInProgress,
|
|
698
713
|
getActiveToolExecutions: lifecycle.getActiveToolExecutions,
|
|
699
|
-
|
|
714
|
+
// Status is composed before the model-switch controller; it reads the controller's state when it renders.
|
|
715
|
+
hasPendingModelSwitch() {
|
|
716
|
+
return modelSwitchController.hasPendingSwitch();
|
|
717
|
+
},
|
|
700
718
|
getQueuedItems: telegramQueueStore.getQueuedItems,
|
|
701
719
|
getQueuedItemCount: Queue.countExecutableTelegramQueueItems,
|
|
702
720
|
formatQueuedStatus: Queue.formatQueuedTelegramItemsStatus,
|
|
@@ -739,6 +757,11 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
739
757
|
const directTelegramApiRuntime =
|
|
740
758
|
TelegramApi.createDefaultTelegramBridgeApiRuntime({
|
|
741
759
|
getBotToken: configStore.getBotToken,
|
|
760
|
+
isThreadedMode() {
|
|
761
|
+
return threadStore.getBotState().threadMode === "enabled" ||
|
|
762
|
+
telegramBusFollowerRegistrationState.getTarget()?.threadId !== undefined ||
|
|
763
|
+
telegramBusLeaderState.getTarget()?.threadId !== undefined;
|
|
764
|
+
},
|
|
742
765
|
getBotScope() {
|
|
743
766
|
const config = configStore.get();
|
|
744
767
|
return (
|
|
@@ -837,9 +860,13 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
837
860
|
lifecycle,
|
|
838
861
|
typing,
|
|
839
862
|
getDefaultChatId: proactivePushChatIdGetter,
|
|
863
|
+
isThreadedMode() {
|
|
864
|
+
return deliveryTargetPolicyRuntime.getTargetPolicyView().threaded === true;
|
|
865
|
+
},
|
|
840
866
|
sendTypingAction,
|
|
841
867
|
updateStatus,
|
|
842
868
|
isContextActive: telegramSessionContextStore.isCurrent,
|
|
869
|
+
getSessionGeneration: telegramSessionContextStore.getGeneration,
|
|
843
870
|
getTransportAuthority() {
|
|
844
871
|
if (ownsTelegramDirectDelivery()) {
|
|
845
872
|
const epoch = getCurrentLeaderEpoch();
|
|
@@ -875,6 +902,11 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
875
902
|
const deliveryTargetPolicyRuntime =
|
|
876
903
|
Delivery.createTelegramDeliveryTargetPolicyRuntime({
|
|
877
904
|
ownsDirect: lockRuntime.owns,
|
|
905
|
+
isThreadedMode() {
|
|
906
|
+
return threadStore.getBotState().threadMode === "enabled" ||
|
|
907
|
+
telegramBusFollowerRegistrationState.getTarget()?.threadId !== undefined ||
|
|
908
|
+
telegramBusLeaderState.getTarget()?.threadId !== undefined;
|
|
909
|
+
},
|
|
878
910
|
isFollowerRegistered: telegramBusFollowerRegistrationState.isRegistered,
|
|
879
911
|
getAllowedChatId: configStore.getAllowedUserId,
|
|
880
912
|
getFollowerTarget: telegramBusFollowerRegistrationState.getTarget,
|
|
@@ -888,6 +920,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
888
920
|
const deliveryLifecycleRuntime =
|
|
889
921
|
Delivery.createTelegramBridgeDeliveryLifecycleHooks({
|
|
890
922
|
generationSeed: deliveryGenerationSeed,
|
|
923
|
+
captureDrain: captureCleanupDrain,
|
|
891
924
|
getTargetPolicyView: deliveryTargetPolicyRuntime.getTargetPolicyView,
|
|
892
925
|
getTransportStamp: telegramTransportStampRuntime.getStamp,
|
|
893
926
|
isTransportStampActive: telegramTransportStampRuntime.isActive,
|
|
@@ -948,6 +981,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
948
981
|
publicationRuntime,
|
|
949
982
|
} = Bindings.createTelegramActivityBindingRuntime({
|
|
950
983
|
generation: deliveryGenerationSeed,
|
|
984
|
+
captureDrain: captureCleanupDrain,
|
|
951
985
|
assistantOutput: {
|
|
952
986
|
prepareTelegramPreview: previewRuntime.preparePublication,
|
|
953
987
|
authority: {
|
|
@@ -1029,8 +1063,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1029
1063
|
const modelSwitchController =
|
|
1030
1064
|
Model.createTelegramModelSwitchControllerRuntime({
|
|
1031
1065
|
isIdle,
|
|
1032
|
-
getPendingModelSwitch: pendingModelSwitchStore.get,
|
|
1033
|
-
setPendingModelSwitch: pendingModelSwitchStore.set,
|
|
1034
1066
|
getActiveTurn: activeTurnRuntime.get,
|
|
1035
1067
|
getAbortHandler: abort.getHandler,
|
|
1036
1068
|
hasAbortHandler: abort.hasHandler,
|
|
@@ -1258,7 +1290,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1258
1290
|
lifecycle: bridgeRuntime.lifecycle,
|
|
1259
1291
|
isIdle,
|
|
1260
1292
|
hasPendingMessages,
|
|
1261
|
-
hasPendingControl:
|
|
1293
|
+
hasPendingControl: modelSwitchController.hasPendingSwitch,
|
|
1262
1294
|
publication: publicationRuntime,
|
|
1263
1295
|
activity: activityRuntime,
|
|
1264
1296
|
delivery: deliveryLifecycleRuntime,
|
|
@@ -1436,6 +1468,14 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
1436
1468
|
)
|
|
1437
1469
|
);
|
|
1438
1470
|
},
|
|
1471
|
+
canUsePreDispatchRebind(recipient) {
|
|
1472
|
+
return lockRuntime.owns() && telegramBusFollowerRegistry.list().every(function (follower) {
|
|
1473
|
+
return Bus.hasTelegramBusCapability(follower.protocol, Bus.TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND);
|
|
1474
|
+
}) && (recipient?.kind !== "follower" || Bus.hasTelegramBusCapability(
|
|
1475
|
+
telegramBusFollowerRegistry.get(recipient.instanceId)?.protocol,
|
|
1476
|
+
Bus.TELEGRAM_BUS_CAPABILITY_PRE_DISPATCH_REBIND_EXECUTION,
|
|
1477
|
+
));
|
|
1478
|
+
},
|
|
1439
1479
|
hasWorkspaceLiveRebindAuthority() {
|
|
1440
1480
|
return (
|
|
1441
1481
|
lockRuntime.owns() &&
|
|
@@ -2193,6 +2233,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
2193
2233
|
followerRegistry: telegramBusFollowerRegistry,
|
|
2194
2234
|
authSecret: telegramBusAuthSecret,
|
|
2195
2235
|
protocolIdentity: telegramBusProtocolIdentity,
|
|
2236
|
+
requiresPreDispatchRebindReader() { return threadStore.hasPreDispatchRebindEvidence(); },
|
|
2196
2237
|
selectedMenuDelivery,
|
|
2197
2238
|
startPolling: pollingAdmissionRuntime.start,
|
|
2198
2239
|
stopPolling: pollingAdmissionRuntime.stop,
|
|
@@ -3029,6 +3070,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
3029
3070
|
isSessionContextActive(ctx) {
|
|
3030
3071
|
return telegramSessionContextStore.isCurrent(ctx);
|
|
3031
3072
|
},
|
|
3073
|
+
getSessionGeneration: telegramSessionContextStore.getGeneration,
|
|
3032
3074
|
isTurnTransportActive(turn) {
|
|
3033
3075
|
return telegramTransportStampRuntime.isActive(turn.transportStamp);
|
|
3034
3076
|
},
|
package/lib/lifecycle.ts
CHANGED
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
UIPromptEndEvent,
|
|
30
30
|
UIPromptStartEvent,
|
|
31
31
|
} from "./pi.ts";
|
|
32
|
+
import * as Machine from "./machine.ts";
|
|
32
33
|
import * as Queue from "./queue.ts";
|
|
33
34
|
import { formatTelegramConnectionFailure } from "./status.ts";
|
|
34
35
|
import * as TextGroups from "./text-groups.ts";
|
|
@@ -411,6 +412,30 @@ export function createTelegramSessionContextStore<TContext>(
|
|
|
411
412
|
};
|
|
412
413
|
}
|
|
413
414
|
|
|
415
|
+
/** Capture drain hints at work admission; late completion never borrows a replacement session or transport. */
|
|
416
|
+
export function createTelegramSessionDrainObserver<TContext, TAuthority>(deps: {
|
|
417
|
+
context: Pick<TelegramSessionContextStore<TContext>, "get" | "getGeneration" | "isCurrent">;
|
|
418
|
+
captureAuthority: () => TAuthority | undefined;
|
|
419
|
+
isAuthorityActive: (authority: TAuthority) => boolean;
|
|
420
|
+
onDrained: (ctx: TContext) => void;
|
|
421
|
+
}): () => (() => void) | undefined {
|
|
422
|
+
return () => {
|
|
423
|
+
try {
|
|
424
|
+
const ctx = deps.context.get(), generation = deps.context.getGeneration(), authority = deps.captureAuthority();
|
|
425
|
+
if (ctx === undefined || authority === undefined) return undefined;
|
|
426
|
+
return () => {
|
|
427
|
+
queueMicrotask(() => {
|
|
428
|
+
try {
|
|
429
|
+
if (!deps.context.isCurrent(ctx, generation) || !deps.isAuthorityActive(authority)) return;
|
|
430
|
+
const emitted: unknown = deps.onDrained(ctx);
|
|
431
|
+
if (emitted) void Promise.resolve(emitted).catch(() => undefined);
|
|
432
|
+
} catch { /* A drain hint is observational, not execution authority. */ }
|
|
433
|
+
});
|
|
434
|
+
};
|
|
435
|
+
} catch { return undefined; }
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
414
439
|
export function createTelegramSessionGenerationFence(
|
|
415
440
|
store: TelegramSessionContextStore<ExtensionContext>,
|
|
416
441
|
hooks: TelegramSessionLifecycleHooks,
|
|
@@ -670,6 +695,121 @@ export interface TelegramCompactionObserverRuntime<TContext> {
|
|
|
670
695
|
onSessionShutdown: () => void;
|
|
671
696
|
}
|
|
672
697
|
|
|
698
|
+
/** The whole compaction-observer state; only `transitionTelegramCompactionObserver` produces a new one. */
|
|
699
|
+
export interface TelegramCompactionObserverState {
|
|
700
|
+
/** The fallback timer's observation; a timer from any other one is inert. */
|
|
701
|
+
readonly observation: number | null;
|
|
702
|
+
/** The typing loop is the observer's to stop; a loop the agent already ran is never stopped by compaction. */
|
|
703
|
+
readonly typingOwned: boolean;
|
|
704
|
+
readonly nextObservation: number;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export type TelegramCompactionObserverEvent =
|
|
708
|
+
| { type: "beforeCompact" }
|
|
709
|
+
/** The outcome of the observer's own typing start, reported back by the shell. */
|
|
710
|
+
| { type: "typingStarted"; started: boolean }
|
|
711
|
+
| { type: "compacted" }
|
|
712
|
+
| { type: "compactFailed" }
|
|
713
|
+
| { type: "timerFired"; observation: number; contextActive: boolean }
|
|
714
|
+
| { type: "shutdown" };
|
|
715
|
+
|
|
716
|
+
export type TelegramCompactionObserverEffect =
|
|
717
|
+
| { type: "setInProgress"; inProgress: boolean }
|
|
718
|
+
| { type: "startTyping" }
|
|
719
|
+
| { type: "stopTyping" }
|
|
720
|
+
| { type: "updateStatus" }
|
|
721
|
+
| { type: "armTimer"; observation: number }
|
|
722
|
+
| { type: "cancelTimer" }
|
|
723
|
+
| { type: "recordTimeout" }
|
|
724
|
+
| { type: "abandoned" }
|
|
725
|
+
| { type: "requestDispatch" };
|
|
726
|
+
|
|
727
|
+
export function createTelegramCompactionObserverState(): TelegramCompactionObserverState {
|
|
728
|
+
return { observation: null, typingOwned: false, nextObservation: 0 };
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Pure compaction-observer transition. A completion or failure always releases presence; the timeout releases only its
|
|
733
|
+
* own observation. Typing ownership is sticky: a repeated start while the observer's own loop runs reports `false`
|
|
734
|
+
* from the shared loop, and must not hand that loop to nobody.
|
|
735
|
+
*/
|
|
736
|
+
export function transitionTelegramCompactionObserver(
|
|
737
|
+
state: TelegramCompactionObserverState,
|
|
738
|
+
event: TelegramCompactionObserverEvent,
|
|
739
|
+
): Machine.TelegramMachineStep<
|
|
740
|
+
TelegramCompactionObserverState,
|
|
741
|
+
TelegramCompactionObserverEffect
|
|
742
|
+
> {
|
|
743
|
+
const cancel: TelegramCompactionObserverEffect[] =
|
|
744
|
+
state.observation === null ? [] : [{ type: "cancelTimer" }];
|
|
745
|
+
const release = (
|
|
746
|
+
...tail: TelegramCompactionObserverEffect[]
|
|
747
|
+
): TelegramCompactionObserverEffect[] => [
|
|
748
|
+
{ type: "setInProgress", inProgress: false },
|
|
749
|
+
...(state.typingOwned ? [{ type: "stopTyping" } as const] : []),
|
|
750
|
+
{ type: "updateStatus" },
|
|
751
|
+
...tail,
|
|
752
|
+
];
|
|
753
|
+
switch (event.type) {
|
|
754
|
+
case "beforeCompact": {
|
|
755
|
+
const observation = state.nextObservation + 1;
|
|
756
|
+
return {
|
|
757
|
+
state: { ...state, observation, nextObservation: observation },
|
|
758
|
+
effects: [
|
|
759
|
+
{ type: "setInProgress", inProgress: true },
|
|
760
|
+
{ type: "startTyping" },
|
|
761
|
+
{ type: "updateStatus" },
|
|
762
|
+
...cancel,
|
|
763
|
+
{ type: "armTimer", observation },
|
|
764
|
+
],
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
case "typingStarted":
|
|
768
|
+
return {
|
|
769
|
+
state: { ...state, typingOwned: state.typingOwned || event.started },
|
|
770
|
+
effects: [],
|
|
771
|
+
};
|
|
772
|
+
case "compacted":
|
|
773
|
+
return {
|
|
774
|
+
state: { ...state, observation: null, typingOwned: false },
|
|
775
|
+
effects: [...cancel, ...release({ type: "requestDispatch" })],
|
|
776
|
+
};
|
|
777
|
+
case "compactFailed":
|
|
778
|
+
return {
|
|
779
|
+
state: { ...state, observation: null, typingOwned: false },
|
|
780
|
+
effects: [
|
|
781
|
+
...cancel,
|
|
782
|
+
...release({ type: "abandoned" }, { type: "requestDispatch" }),
|
|
783
|
+
],
|
|
784
|
+
};
|
|
785
|
+
case "timerFired":
|
|
786
|
+
if (state.observation !== event.observation)
|
|
787
|
+
return { state, effects: [] };
|
|
788
|
+
// A replaced context consumes its observation without releasing the replacement's presence.
|
|
789
|
+
if (!event.contextActive)
|
|
790
|
+
return { state: { ...state, observation: null }, effects: [] };
|
|
791
|
+
return {
|
|
792
|
+
state: { ...state, observation: null, typingOwned: false },
|
|
793
|
+
effects: release(
|
|
794
|
+
{ type: "recordTimeout" },
|
|
795
|
+
{ type: "requestDispatch" },
|
|
796
|
+
),
|
|
797
|
+
};
|
|
798
|
+
case "shutdown":
|
|
799
|
+
return {
|
|
800
|
+
state: { ...state, observation: null, typingOwned: false },
|
|
801
|
+
effects: [
|
|
802
|
+
...cancel,
|
|
803
|
+
...(state.typingOwned ? [{ type: "stopTyping" } as const] : []),
|
|
804
|
+
],
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* The shell around `transitionTelegramCompactionObserver`: it fences inactive contexts, runs effects with the calling
|
|
811
|
+
* context, reports the typing start back and owns the fallback timer, which keeps the context it was armed under.
|
|
812
|
+
*/
|
|
673
813
|
export function createTelegramCompactionObserverRuntime<TContext>(
|
|
674
814
|
deps: TelegramCompactionObserverRuntimeDeps<TContext>,
|
|
675
815
|
): TelegramCompactionObserverRuntime<TContext> {
|
|
@@ -677,74 +817,105 @@ export function createTelegramCompactionObserverRuntime<TContext>(
|
|
|
677
817
|
const setTimer = deps.setTimer ?? setTimeout;
|
|
678
818
|
const clearTimer = deps.clearTimer ?? clearTimeout;
|
|
679
819
|
let fallbackTimer: TelegramLifecycleTimer | undefined;
|
|
680
|
-
|
|
681
|
-
let
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
820
|
+
// The context of the event being dispatched; effects run synchronously inside that dispatch.
|
|
821
|
+
let call: TContext | undefined;
|
|
822
|
+
const machine = Machine.createTelegramMachineRunner<
|
|
823
|
+
TelegramCompactionObserverState,
|
|
824
|
+
TelegramCompactionObserverEvent,
|
|
825
|
+
TelegramCompactionObserverEffect
|
|
826
|
+
>({
|
|
827
|
+
initial: createTelegramCompactionObserverState(),
|
|
828
|
+
transition: transitionTelegramCompactionObserver,
|
|
829
|
+
run(effect) {
|
|
830
|
+
const ctx = call as TContext;
|
|
831
|
+
switch (effect.type) {
|
|
832
|
+
case "setInProgress":
|
|
833
|
+
deps.setCompactionInProgress(effect.inProgress);
|
|
834
|
+
return;
|
|
835
|
+
case "startTyping": {
|
|
836
|
+
const result = deps.startTypingLoop?.(ctx);
|
|
837
|
+
machine.dispatch({
|
|
838
|
+
type: "typingStarted",
|
|
839
|
+
started: !!deps.startTypingLoop && result !== false,
|
|
840
|
+
});
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
case "stopTyping":
|
|
844
|
+
deps.stopTypingLoop?.();
|
|
845
|
+
return;
|
|
846
|
+
case "updateStatus":
|
|
847
|
+
deps.updateStatus(ctx);
|
|
848
|
+
return;
|
|
849
|
+
case "armTimer": {
|
|
850
|
+
const armedCtx = ctx;
|
|
851
|
+
fallbackTimer = setTimer(() => {
|
|
852
|
+
fallbackTimer = undefined;
|
|
853
|
+
dispatchFor(armedCtx, {
|
|
854
|
+
type: "timerFired",
|
|
855
|
+
observation: effect.observation,
|
|
856
|
+
contextActive:
|
|
857
|
+
!deps.isContextActive || deps.isContextActive(armedCtx),
|
|
858
|
+
});
|
|
859
|
+
}, timeoutMs);
|
|
860
|
+
unrefTelegramLifecycleTimer(fallbackTimer);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
case "cancelTimer":
|
|
864
|
+
if (fallbackTimer !== undefined) clearTimer(fallbackTimer);
|
|
865
|
+
fallbackTimer = undefined;
|
|
866
|
+
return;
|
|
867
|
+
case "recordTimeout":
|
|
868
|
+
deps.recordRuntimeEvent?.(
|
|
869
|
+
"compact",
|
|
870
|
+
new Error("Compaction observer timed out"),
|
|
871
|
+
);
|
|
872
|
+
return;
|
|
873
|
+
case "abandoned":
|
|
874
|
+
deps.onCompactionAbandoned?.();
|
|
875
|
+
return;
|
|
876
|
+
case "requestDispatch":
|
|
877
|
+
// Observer expiry releases local presence, not Pi's eventual terminal result.
|
|
878
|
+
deps.requestDeferredDispatchNextQueuedTelegramTurn(
|
|
879
|
+
deps.dispatchNextQueuedTelegramTurn,
|
|
880
|
+
);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
const dispatchFor = (
|
|
886
|
+
ctx: TContext | undefined,
|
|
887
|
+
event: TelegramCompactionObserverEvent,
|
|
888
|
+
): void => {
|
|
889
|
+
const previous = call;
|
|
890
|
+
call = ctx;
|
|
891
|
+
try {
|
|
892
|
+
machine.dispatch(event);
|
|
893
|
+
} finally {
|
|
894
|
+
call = previous;
|
|
895
|
+
}
|
|
699
896
|
};
|
|
897
|
+
const isActive = (ctx: TContext): boolean =>
|
|
898
|
+
!deps.isContextActive || deps.isContextActive(ctx);
|
|
700
899
|
return {
|
|
701
900
|
onSessionBeforeCompact: (_event, ctx) => {
|
|
702
|
-
if (
|
|
703
|
-
deps.setCompactionInProgress(true);
|
|
704
|
-
const typingStartResult = deps.startTypingLoop?.(ctx);
|
|
705
|
-
typingStartedByObserver =
|
|
706
|
-
!!deps.startTypingLoop && typingStartResult !== false;
|
|
707
|
-
deps.updateStatus(ctx);
|
|
708
|
-
clearFallbackTimer();
|
|
709
|
-
const admittedGeneration = observationGeneration;
|
|
710
|
-
fallbackTimer = setTimer(() => {
|
|
711
|
-
if (observationGeneration !== admittedGeneration) return;
|
|
712
|
-
observationGeneration += 1;
|
|
713
|
-
fallbackTimer = undefined;
|
|
714
|
-
if (deps.isContextActive && !deps.isContextActive(ctx)) return;
|
|
715
|
-
releaseCompactionPresence(ctx);
|
|
716
|
-
deps.recordRuntimeEvent?.(
|
|
717
|
-
"compact",
|
|
718
|
-
new Error("Compaction observer timed out"),
|
|
719
|
-
);
|
|
720
|
-
// Observer expiry releases local presence, not Pi's eventual terminal result.
|
|
721
|
-
requestDispatch();
|
|
722
|
-
}, timeoutMs);
|
|
723
|
-
unrefTelegramLifecycleTimer(fallbackTimer);
|
|
901
|
+
if (isActive(ctx)) dispatchFor(ctx, { type: "beforeCompact" });
|
|
724
902
|
},
|
|
725
903
|
onSessionCompact: (_event, ctx) => {
|
|
726
|
-
if (
|
|
727
|
-
clearFallbackTimer();
|
|
728
|
-
releaseCompactionPresence(ctx);
|
|
729
|
-
requestDispatch();
|
|
904
|
+
if (isActive(ctx)) dispatchFor(ctx, { type: "compacted" });
|
|
730
905
|
},
|
|
731
906
|
onSessionCompactFailed: (_event, ctx) => {
|
|
732
|
-
if (
|
|
733
|
-
clearFallbackTimer();
|
|
734
|
-
releaseCompactionPresence(ctx);
|
|
735
|
-
deps.onCompactionAbandoned?.();
|
|
736
|
-
requestDispatch();
|
|
907
|
+
if (isActive(ctx)) dispatchFor(ctx, { type: "compactFailed" });
|
|
737
908
|
},
|
|
738
909
|
onSessionShutdown: () => {
|
|
739
|
-
|
|
740
|
-
if (typingStartedByObserver) deps.stopTypingLoop?.();
|
|
741
|
-
typingStartedByObserver = false;
|
|
910
|
+
dispatchFor(undefined, { type: "shutdown" });
|
|
742
911
|
},
|
|
743
912
|
};
|
|
744
913
|
}
|
|
745
914
|
|
|
746
915
|
export interface TelegramMessageActivityTypingDeps<TContext> {
|
|
747
916
|
hasActiveTurn: () => boolean;
|
|
917
|
+
isContextActive?: (ctx: TContext) => boolean;
|
|
918
|
+
getSessionGeneration?: () => number;
|
|
748
919
|
startTypingLoop: (ctx: TContext) => void;
|
|
749
920
|
onMessageStart: TelegramLifecycleRegistrationDeps["onMessageStart"];
|
|
750
921
|
onMessageUpdate: TelegramLifecycleRegistrationDeps["onMessageUpdate"];
|
|
@@ -773,13 +944,16 @@ export function createTelegramMessageActivityTypingHooks<
|
|
|
773
944
|
inner: (event: TEvent, ctx: ExtensionContext) => Promise<void>,
|
|
774
945
|
): Promise<void> => {
|
|
775
946
|
const typedCtx = ctx as TContext;
|
|
776
|
-
|
|
947
|
+
const generation = deps.getSessionGeneration?.();
|
|
948
|
+
const isCurrent = () => deps.isContextActive?.(typedCtx) !== false &&
|
|
949
|
+
(!deps.getSessionGeneration || Object.is(deps.getSessionGeneration(), generation));
|
|
950
|
+
if (isCurrent()) ensureTyping(typedCtx);
|
|
777
951
|
try {
|
|
778
952
|
await inner(event, ctx);
|
|
779
953
|
} catch (error) {
|
|
780
954
|
deps.recordRuntimeEvent?.("message-activity", error, { phase });
|
|
781
955
|
} finally {
|
|
782
|
-
ensureTyping(typedCtx);
|
|
956
|
+
if (isCurrent()) ensureTyping(typedCtx);
|
|
783
957
|
}
|
|
784
958
|
};
|
|
785
959
|
return {
|
package/lib/machine.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
|
|
8
|
+
/** One transition result: the next state plus effects as data. A machine may add fields, such as a call result. */
|
|
9
|
+
export interface TelegramMachineStep<State, Effect> {
|
|
10
|
+
state: State;
|
|
11
|
+
effects: readonly Effect[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TelegramMachineRunner<State, Event, Step> {
|
|
15
|
+
readonly state: State;
|
|
16
|
+
/** Commits the next state, runs its effects (which may dispatch again), wakes waiters and returns the step. */
|
|
17
|
+
dispatch(event: Event): Step;
|
|
18
|
+
/** Resolves once `ready` holds for the committed state, checking now and after every later dispatch. */
|
|
19
|
+
waitFor(ready: (state: State) => boolean): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createTelegramMachineRunner<
|
|
23
|
+
State,
|
|
24
|
+
Event,
|
|
25
|
+
Effect,
|
|
26
|
+
Step extends TelegramMachineStep<State, Effect> = TelegramMachineStep<
|
|
27
|
+
State,
|
|
28
|
+
Effect
|
|
29
|
+
>,
|
|
30
|
+
>(options: {
|
|
31
|
+
initial: State;
|
|
32
|
+
transition: (state: State, event: Event) => Step;
|
|
33
|
+
run: (effect: Effect) => void;
|
|
34
|
+
}): TelegramMachineRunner<State, Event, Step> {
|
|
35
|
+
let state = options.initial;
|
|
36
|
+
const waiters = new Set<() => void>();
|
|
37
|
+
return {
|
|
38
|
+
get state(): State {
|
|
39
|
+
return state;
|
|
40
|
+
},
|
|
41
|
+
dispatch(event: Event): Step {
|
|
42
|
+
const next = options.transition(state, event);
|
|
43
|
+
state = next.state;
|
|
44
|
+
for (const effect of next.effects) options.run(effect);
|
|
45
|
+
for (const waiter of [...waiters]) waiter();
|
|
46
|
+
return next;
|
|
47
|
+
},
|
|
48
|
+
waitFor(ready: (state: State) => boolean): Promise<void> {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
if (ready(state)) return resolve();
|
|
51
|
+
const waiter = (): void => {
|
|
52
|
+
if (!ready(state)) return;
|
|
53
|
+
waiters.delete(waiter);
|
|
54
|
+
resolve();
|
|
55
|
+
};
|
|
56
|
+
waiters.add(waiter);
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|