@llblab/pi-telegram 0.56.0 → 0.56.2
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 -4
- package/BACKLOG.md +2 -0
- package/CHANGELOG.md +11 -0
- package/README.md +4 -2
- package/dist/lib/bus-follower.d.ts +1 -0
- package/dist/lib/bus-follower.js +6 -5
- package/dist/lib/bus-leader.d.ts +2 -0
- package/dist/lib/bus-leader.js +26 -20
- package/dist/lib/bus.d.ts +5 -0
- package/dist/lib/bus.js +15 -7
- package/dist/lib/commands.d.ts +1 -1
- package/dist/lib/commands.js +33 -28
- package/dist/lib/extension.js +4 -1
- package/dist/lib/locks.d.ts +6 -2
- package/dist/lib/locks.js +3 -3
- package/dist/lib/menu-queue.js +5 -7
- package/dist/lib/polling.d.ts +6 -2
- package/dist/lib/polling.js +2 -2
- package/dist/lib/routing.d.ts +4 -0
- package/dist/lib/routing.js +62 -1
- package/dist/lib/sync.js +11 -1
- package/dist/lib/threads.d.ts +11 -0
- package/dist/lib/threads.js +57 -6
- package/dist/lib/updates.d.ts +6 -27
- package/dist/lib/updates.js +25 -12
- package/dist/package.json +1 -1
- package/docs/architecture.md +12 -2
- package/docs/multi-instance-bus.md +6 -4
- package/docs/ui-style.md +4 -8
- package/lib/bus-follower.ts +8 -5
- package/lib/bus-leader.ts +28 -23
- package/lib/bus.ts +16 -7
- package/lib/commands.ts +52 -47
- package/lib/extension.ts +4 -1
- package/lib/locks.ts +5 -2
- package/lib/menu-queue.ts +6 -8
- package/lib/polling.ts +4 -1
- package/lib/routing.ts +51 -1
- package/lib/sync.ts +11 -0
- package/lib/threads.ts +57 -5
- package/lib/updates.ts +23 -12
- package/package.json +1 -1
package/lib/threads.ts
CHANGED
|
@@ -783,6 +783,7 @@ export interface TelegramTopicTargetStore {
|
|
|
783
783
|
) => { slot?: string; threadName?: string } | undefined;
|
|
784
784
|
listReservations: () => TelegramThreadReservation[];
|
|
785
785
|
listPendingProvisions: () => TelegramThreadPendingProvision[];
|
|
786
|
+
resetPendingProvisionForConnect: (expected: TelegramThreadPendingProvision, isCurrent: () => boolean) => Promise<boolean>;
|
|
786
787
|
listPendingCleanups: () => TelegramThreadCleanupIntent[];
|
|
787
788
|
listSyncObservations: () => TelegramTopicSyncObservation[];
|
|
788
789
|
reserveThread: (reservation: TelegramThreadReservation) => void;
|
|
@@ -902,6 +903,7 @@ export interface TelegramTopicTargetStore {
|
|
|
902
903
|
options?: {
|
|
903
904
|
existingBindingOnly?: boolean;
|
|
904
905
|
sessionId?: string;
|
|
906
|
+
provisionProfileKey?: string;
|
|
905
907
|
onCapacityUnavailable?: () => void;
|
|
906
908
|
},
|
|
907
909
|
) => TelegramWorkspaceBindingIdentity | undefined;
|
|
@@ -4168,6 +4170,7 @@ export function createTelegramTopicTargetStore(
|
|
|
4168
4170
|
}
|
|
4169
4171
|
),
|
|
4170
4172
|
isCurrentPublication?: () => boolean,
|
|
4173
|
+
resetProvisions?: readonly TelegramThreadPendingProvision[],
|
|
4171
4174
|
): Promise<boolean> => {
|
|
4172
4175
|
const submittedPath = getPath(),
|
|
4173
4176
|
submittedProfile = activeProfile();
|
|
@@ -4367,6 +4370,13 @@ export function createTelegramTopicTargetStore(
|
|
|
4367
4370
|
!!expectedRestore,
|
|
4368
4371
|
),
|
|
4369
4372
|
);
|
|
4373
|
+
if (resetProvisions) {
|
|
4374
|
+
const recoveries = readProvisionRecoveries(path, true);
|
|
4375
|
+
if (mutationRevision !== persistedRevision || resetProvisions.some(expected =>
|
|
4376
|
+
expected.target || Object.hasOwn(recoveries, expected.id) ||
|
|
4377
|
+
!isDeepStrictEqual(disk.pendingProvisions?.find(value => value.id === expected.id), expected)))
|
|
4378
|
+
throw new Error("Telegram connection recovery provisioning evidence changed.");
|
|
4379
|
+
}
|
|
4370
4380
|
if (!isDeepStrictEqual(disk.workspaceRestore, expectedRestore))
|
|
4371
4381
|
throw new Error(
|
|
4372
4382
|
"Workspace Restore evidence changed before publication.",
|
|
@@ -4429,7 +4439,7 @@ export function createTelegramTopicTargetStore(
|
|
|
4429
4439
|
}
|
|
4430
4440
|
: {}),
|
|
4431
4441
|
reservations: reservations.map((reservation) => ({ ...reservation })),
|
|
4432
|
-
pendingProvisions: pendingProvisions.map((provision) => ({
|
|
4442
|
+
pendingProvisions: pendingProvisions.filter(provision => !resetProvisions?.some(expected => expected.id === provision.id)).map((provision) => ({
|
|
4433
4443
|
...provision,
|
|
4434
4444
|
...(provision.target ? { target: { ...provision.target } } : {}),
|
|
4435
4445
|
})),
|
|
@@ -4636,6 +4646,7 @@ export function createTelegramTopicTargetStore(
|
|
|
4636
4646
|
loadedProfile = submittedProfile;
|
|
4637
4647
|
loaded = true;
|
|
4638
4648
|
observedWorkspaceSemantic = getTelegramStateSemanticSnapshot(wire);
|
|
4649
|
+
if (resetProvisions) { pendingProvisions = file.pendingProvisions; mutationRevision += 1; }
|
|
4639
4650
|
if (transition) {
|
|
4640
4651
|
records = new Map(
|
|
4641
4652
|
file.threads.map((record) => {
|
|
@@ -4808,6 +4819,10 @@ export function createTelegramTopicTargetStore(
|
|
|
4808
4819
|
if (isCurrentPublication?.() === false) return false;
|
|
4809
4820
|
loadedPath = path;
|
|
4810
4821
|
loaded = true;
|
|
4822
|
+
if (resetProvisions) {
|
|
4823
|
+
if (mutationRevision !== persistedRevision) return false;
|
|
4824
|
+
pendingProvisions = file.pendingProvisions;
|
|
4825
|
+
}
|
|
4811
4826
|
if (mutationRevision === persistedRevision) dirty = false;
|
|
4812
4827
|
return true;
|
|
4813
4828
|
});
|
|
@@ -6814,6 +6829,11 @@ export function createTelegramTopicTargetStore(
|
|
|
6814
6829
|
current.status = "ambiguous";
|
|
6815
6830
|
return true;
|
|
6816
6831
|
},
|
|
6832
|
+
resetPendingProvisionForConnect(expected, isCurrent) {
|
|
6833
|
+
// Stage no in-memory deletion: refusal or a stale publication must keep its protective hold.
|
|
6834
|
+
if (expected.target || !isCurrent()) return Promise.resolve(false);
|
|
6835
|
+
return persistSnapshot(undefined, isCurrent, [structuredClone(expected)]);
|
|
6836
|
+
},
|
|
6817
6837
|
removePendingProvision(id) {
|
|
6818
6838
|
const before = pendingProvisions.length;
|
|
6819
6839
|
pendingProvisions = pendingProvisions.filter(
|
|
@@ -7608,8 +7628,14 @@ export function createTelegramTopicTargetStore(
|
|
|
7608
7628
|
) {
|
|
7609
7629
|
return undefined;
|
|
7610
7630
|
}
|
|
7611
|
-
const
|
|
7612
|
-
|
|
7631
|
+
const ownedProvisions = pendingProvisions.filter(value =>
|
|
7632
|
+
value.workspaceBindingKey === identity.bindingKey &&
|
|
7633
|
+
(value.instanceId === instanceId || value.instanceId === effectivePreviousInstanceId ||
|
|
7634
|
+
(options?.provisionProfileKey !== undefined && value.profileKey === options.provisionProfileKey)));
|
|
7635
|
+
if (ownedProvisions.length > 1) return undefined;
|
|
7636
|
+
const ownProvision = ownedProvisions[0];
|
|
7637
|
+
const retainedTarget = ownProvision?.target ?? binding?.target ?? legacyRecord?.target;
|
|
7638
|
+
const retainedSlot = ownProvision?.slot ?? binding?.slot ?? legacyRecord?.slot;
|
|
7613
7639
|
const otherBindings = Array.from(workspaceBindings.values())
|
|
7614
7640
|
.filter(
|
|
7615
7641
|
(other) => other.bindingKey !== identity.bindingKey && other.slot,
|
|
@@ -7641,7 +7667,7 @@ export function createTelegramTopicTargetStore(
|
|
|
7641
7667
|
)
|
|
7642
7668
|
.map((reservation) => reservation.slot),
|
|
7643
7669
|
...pendingProvisions
|
|
7644
|
-
.filter((provision) =>
|
|
7670
|
+
.filter((provision) => provision !== ownProvision &&
|
|
7645
7671
|
isPendingProvisionLiveOrTargeted(provision, nowMs),
|
|
7646
7672
|
)
|
|
7647
7673
|
.map((provision) => provision.slot),
|
|
@@ -9661,6 +9687,32 @@ export function createTelegramTopicTargetRenamer(
|
|
|
9661
9687
|
};
|
|
9662
9688
|
}
|
|
9663
9689
|
|
|
9690
|
+
/** Caller holds profile Workspace admission; only an explicit connect grants a fresh creation attempt. */
|
|
9691
|
+
export async function resetTelegramConnectionProvisioning(input: {
|
|
9692
|
+
store: TelegramTopicTargetStore;
|
|
9693
|
+
owner: TelegramThreadPendingProvision["owner"];
|
|
9694
|
+
instanceId: string;
|
|
9695
|
+
profileKey: string;
|
|
9696
|
+
isCurrent: () => boolean;
|
|
9697
|
+
recordEvent: (category: string, message: unknown, details?: Record<string, unknown>) => void;
|
|
9698
|
+
}): Promise<void> {
|
|
9699
|
+
if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
|
|
9700
|
+
const removed: TelegramThreadPendingProvision[] = [];
|
|
9701
|
+
for (const pending of input.store.listPendingProvisions()) {
|
|
9702
|
+
if (pending.owner !== input.owner || pending.target ||
|
|
9703
|
+
(pending.profileKey !== input.profileKey && pending.instanceId !== input.instanceId)) continue;
|
|
9704
|
+
if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
|
|
9705
|
+
if (!await input.store.resetPendingProvisionForConnect(pending, input.isCurrent))
|
|
9706
|
+
throw new Error("Telegram connection recovery lost publication authority.");
|
|
9707
|
+
if (!input.isCurrent()) throw new Error("Telegram connection recovery authority changed.");
|
|
9708
|
+
removed.push(pending);
|
|
9709
|
+
}
|
|
9710
|
+
if (removed.length === 0) return;
|
|
9711
|
+
input.recordEvent("connection", "Explicit connect reset unfinished Thread creation; an unconfirmed Telegram Thread may remain.", {
|
|
9712
|
+
phase: "connect-provision-reset", count: removed.length, owner: input.owner,
|
|
9713
|
+
});
|
|
9714
|
+
}
|
|
9715
|
+
|
|
9664
9716
|
export function createTelegramTopicTargetProvisioner(
|
|
9665
9717
|
deps: TelegramTopicTargetProvisionerDeps,
|
|
9666
9718
|
): (
|
|
@@ -9923,7 +9975,7 @@ export function createTelegramTopicTargetProvisioner(
|
|
|
9923
9975
|
slot,
|
|
9924
9976
|
);
|
|
9925
9977
|
}
|
|
9926
|
-
const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}`;
|
|
9978
|
+
const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}:${randomUUID()}`;
|
|
9927
9979
|
const pendingOwner =
|
|
9928
9980
|
request.owner?.kind === "leader" ? "leader" : "manual-follower";
|
|
9929
9981
|
assertLeaderEpoch("before-pending-intent");
|
package/lib/updates.ts
CHANGED
|
@@ -108,16 +108,10 @@ export type TelegramReactionType =
|
|
|
108
108
|
|
|
109
109
|
export const TELEGRAM_PRIORITY_REACTIONS = [
|
|
110
110
|
{ id: 10, name: "like", emoji: "👍" },
|
|
111
|
-
{ id: 12, name: "heart", emoji: "❤" },
|
|
112
|
-
{ id: 13, name: "dove", emoji: "🕊" },
|
|
113
111
|
] as const;
|
|
114
112
|
export const TELEGRAM_STEER_REACTIONS = [{ id: 14, name: "fire", emoji: "🔥" }] as const;
|
|
115
113
|
export const TELEGRAM_REMOVAL_REACTIONS = [
|
|
116
114
|
{ id: 20, name: "dislike", emoji: "👎" },
|
|
117
|
-
{ id: 21, name: "ghost", emoji: "👻" },
|
|
118
|
-
{ id: 22, name: "broken-heart", emoji: "💔" },
|
|
119
|
-
{ id: 23, name: "poop", emoji: "💩" },
|
|
120
|
-
{ id: 24, name: "wastebasket", emoji: "🗑" },
|
|
121
115
|
] as const;
|
|
122
116
|
export const TELEGRAM_PRIORITY_REACTION_EMOJIS =
|
|
123
117
|
TELEGRAM_PRIORITY_REACTIONS.map((reaction) => reaction.emoji);
|
|
@@ -159,7 +153,7 @@ function getTelegramQueueReactionTransition(
|
|
|
159
153
|
): TelegramQueueReactionDisposition | undefined {
|
|
160
154
|
const oldEmojis = collectTelegramReactionEmojis(oldReactions);
|
|
161
155
|
const newEmojis = collectTelegramReactionEmojis(newReactions);
|
|
162
|
-
// Fire
|
|
156
|
+
// Fire selects Steer; thumbs-up beneath it does not reorder the lane. Other emoji carry no queue intent.
|
|
163
157
|
const steerEmoji = TELEGRAM_STEER_REACTIONS[0].emoji;
|
|
164
158
|
const oldPriorityEmoji = oldEmojis.has(steerEmoji) ? steerEmoji : getTelegramReactionEmoji(oldEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
|
|
165
159
|
const newPriorityEmoji = newEmojis.has(steerEmoji) ? steerEmoji : getTelegramReactionEmoji(newEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS);
|
|
@@ -547,6 +541,7 @@ interface TelegramUpdateAdmissionBinding {
|
|
|
547
541
|
inspectSource?: () => TelegramDeferredSourceEvidence | undefined;
|
|
548
542
|
inspectSourceSnapshot?: () => TelegramDeferredSourceSnapshot | undefined;
|
|
549
543
|
inspectCompletion?: () => TelegramDeferredSourceEvidence | undefined;
|
|
544
|
+
subscribeCompletion?: (notify: () => void) => () => void;
|
|
550
545
|
isSourceUnsettled?: () => boolean;
|
|
551
546
|
prepareLiveInput?: (
|
|
552
547
|
sourceUpdateIds: readonly number[],
|
|
@@ -615,6 +610,7 @@ export function bindTelegramUpdateAdmissionSource<
|
|
|
615
610
|
| "inspectSource"
|
|
616
611
|
| "inspectSourceSnapshot"
|
|
617
612
|
| "inspectCompletion"
|
|
613
|
+
| "subscribeCompletion"
|
|
618
614
|
| "isSourceUnsettled"
|
|
619
615
|
| "acknowledgeTransferred"
|
|
620
616
|
| "prepareLiveInput"
|
|
@@ -743,6 +739,15 @@ export function inspectTelegramDeferredSourceCompletion(
|
|
|
743
739
|
return execution.isCurrent() && source ? { ...source } : undefined;
|
|
744
740
|
}
|
|
745
741
|
|
|
742
|
+
/** Warm ACK notification only; subscribers must independently recheck exact completion and recipient authority. */
|
|
743
|
+
export function subscribeTelegramDeferredSourceCompletion(value: unknown, notify: () => void): (() => void) | undefined {
|
|
744
|
+
const execution = getTelegramUpdateExecutionFence(value);
|
|
745
|
+
if (execution?.isCurrent() !== true) return undefined;
|
|
746
|
+
return getTelegramUpdateAdmissionBinding(value)?.subscribeCompletion?.(() => {
|
|
747
|
+
if (execution.isCurrent()) notify();
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
|
|
746
751
|
/** Reuse only live bound deferred originals; raw IDs or cold/adopted input cannot mint this carrier. */
|
|
747
752
|
export function prepareTelegramLiveDeferredInput(
|
|
748
753
|
values: readonly unknown[],
|
|
@@ -8721,6 +8726,7 @@ function createTelegramUpdateSourceAdmissionHandle<
|
|
|
8721
8726
|
let outcome: TelegramUpdateAdmissionOutcome | undefined;
|
|
8722
8727
|
let completionAttempted = false;
|
|
8723
8728
|
let completedSource: TelegramDeferredSourceCompletion | undefined;
|
|
8729
|
+
const completionObservers = new Set<() => void>();
|
|
8724
8730
|
const boundUpdate = bindTelegramUpdateExecutionFence(
|
|
8725
8731
|
bindTelegramUpdateAdmissionSource(
|
|
8726
8732
|
update,
|
|
@@ -8777,11 +8783,12 @@ function createTelegramUpdateSourceAdmissionHandle<
|
|
|
8777
8783
|
execution.isCurrent() &&
|
|
8778
8784
|
completion.isCurrent() &&
|
|
8779
8785
|
isDeepStrictEqual(completion.source, expectedSource)
|
|
8780
|
-
)
|
|
8781
|
-
completedSource = {
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
}
|
|
8786
|
+
) {
|
|
8787
|
+
completedSource = { ...completion, source: { ...expectedSource } };
|
|
8788
|
+
for (const notify of completionObservers) {
|
|
8789
|
+
try { notify(); } catch { /* Observation cannot invalidate an exact removal ACK. */ }
|
|
8790
|
+
}
|
|
8791
|
+
}
|
|
8785
8792
|
})
|
|
8786
8793
|
.catch((error) => {
|
|
8787
8794
|
try {
|
|
@@ -8842,6 +8849,10 @@ function createTelegramUpdateSourceAdmissionHandle<
|
|
|
8842
8849
|
signal,
|
|
8843
8850
|
})
|
|
8844
8851
|
: undefined,
|
|
8852
|
+
subscribeCompletion: (notify) => {
|
|
8853
|
+
completionObservers.add(notify);
|
|
8854
|
+
return () => { completionObservers.delete(notify); };
|
|
8855
|
+
},
|
|
8845
8856
|
inspectCompletion: () =>
|
|
8846
8857
|
execution.isCurrent() && completedSource?.isCurrent()
|
|
8847
8858
|
? { ...completedSource.source }
|