@llblab/pi-kit 0.26.0 → 0.27.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/BACKLOG.md +2 -2
- package/CHANGELOG.md +5 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +12 -5
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +9 -0
- package/node_modules/@llblab/pi-state-flow/README.md +116 -34
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.d.ts +24 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/acquisition.js +80 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.d.ts +17 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +40 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +149 -298
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +58 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.d.ts +37 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/operation.js +59 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.d.ts +31 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/ownership.js +117 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +7 -7
- package/node_modules/@llblab/pi-state-flow/dist/lib/query.js +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +2 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +28 -0
- package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +6 -1
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +14 -6
- package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-memory/SKILL.md +2 -2
- package/node_modules/@llblab/pi-state-flow/docs/README.md +19 -9
- package/node_modules/@llblab/pi-state-flow/docs/agent-contract-relocation.md +4 -4
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +644 -91
- package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +116 -37
- package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +118 -21
- package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +68 -8
- package/node_modules/@llblab/pi-state-flow/docs/lazy-state.md +88 -14
- package/node_modules/@llblab/pi-state-flow/docs/performance.md +83 -66
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +391 -62
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +317 -62
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +85 -1
- package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +47 -0
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +161 -295
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +63 -0
- package/node_modules/@llblab/pi-state-flow/lib/operation.ts +75 -0
- package/node_modules/@llblab/pi-state-flow/lib/ownership.ts +120 -0
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +7 -8
- package/node_modules/@llblab/pi-state-flow/lib/query.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +1 -1
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +1 -2
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +5 -1
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +14 -6
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +2 -2
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
- package/node_modules/@llblab/pi-telegram/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +4 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +16 -12
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +7 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +12 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +137 -83
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +26 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +57 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +109 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/model.js +2 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +3 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +31 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +4 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +1 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +17 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +48 -12
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -5
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -0
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +17 -10
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +8 -2
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +135 -97
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +25 -0
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +140 -4
- package/node_modules/@llblab/pi-telegram/lib/model.ts +2 -4
- package/node_modules/@llblab/pi-telegram/lib/status.ts +30 -1
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +21 -3
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +46 -13
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
|
@@ -1,8 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram status rendering helpers
|
|
3
3
|
* Zones: telegram ui, pi agent diagnostics, tui
|
|
4
|
-
*
|
|
4
|
+
* Owns status summaries, redacted runtime diagnostics, and compact connection-failure copy
|
|
5
5
|
*/
|
|
6
|
+
/** UI copy is allowlisted; raw exception text belongs only in redacted diagnostics. */
|
|
7
|
+
export function formatTelegramConnectionFailure(error) {
|
|
8
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
9
|
+
const status = error && typeof error === "object" && "status" in error ? error.status : undefined;
|
|
10
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
|
|
11
|
+
if (status === 401)
|
|
12
|
+
return "Telegram token rejected. Run /telegram-setup.";
|
|
13
|
+
if (status === 403)
|
|
14
|
+
return "Telegram access denied. Check /telegram-status --debug.";
|
|
15
|
+
if ((typeof code === "string" && ["ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND", "ECONNRESET"].includes(code)) ||
|
|
16
|
+
/network unavailable|fetch failed/i.test(message)) {
|
|
17
|
+
return "Telegram network unavailable. Retry /telegram-connect.";
|
|
18
|
+
}
|
|
19
|
+
if (/Workspace slots?.*(?:unavailable|exhausted)|no free.*slot/i.test(message)) {
|
|
20
|
+
return "No Telegram slot available. Check /telegram-status --debug.";
|
|
21
|
+
}
|
|
22
|
+
if (code === "incompatible-protocol" || /protocol.*(?:incompatible|mismatch)|incompatible.*protocol/i.test(message)) {
|
|
23
|
+
return "Telegram instances are incompatible. Update them together.";
|
|
24
|
+
}
|
|
25
|
+
if (/unsupported.*(?:journal|version)|(?:journal|version).*unsupported/i.test(message)) {
|
|
26
|
+
return "Telegram state version unsupported. Use a compatible runtime.";
|
|
27
|
+
}
|
|
28
|
+
if (/follower registration failed|active in another Pi instance/i.test(message)) {
|
|
29
|
+
return "Telegram leader is active but unavailable. Check /telegram-status --debug.";
|
|
30
|
+
}
|
|
31
|
+
if (/unfinished Thread creation does not match/i.test(message)) {
|
|
32
|
+
return "Telegram Thread creation is unresolved. Check /telegram-status --debug.";
|
|
33
|
+
}
|
|
34
|
+
return "Telegram connection failed. Check /telegram-status --debug.";
|
|
35
|
+
}
|
|
6
36
|
const TELEGRAM_STATUS_DEFAULT_PROFILE_NAME = "default";
|
|
7
37
|
const TELEGRAM_STATUS_LINE_PROVIDER_REGISTRY_KEY = "__piTelegramStatusLineProviders__";
|
|
8
38
|
const MAX_RECENT_TELEGRAM_RUNTIME_EVENTS = 10;
|
|
@@ -459,6 +459,7 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
|
|
|
459
459
|
? deps.topicTargetStore.getWorkspaceBinding(workspaceIdentity.cwd, workspaceIdentity.instanceSlot, workspaceIdentity.sessionId)
|
|
460
460
|
: undefined;
|
|
461
461
|
const legacyWorkspaceBinding = workspaceIdentity?.instanceSlot === "a" &&
|
|
462
|
+
deps.sessionId === undefined &&
|
|
462
463
|
!persistedWorkspaceBinding &&
|
|
463
464
|
typeof legacyLeaderRecord?.target.threadId === "number"
|
|
464
465
|
? {
|
|
@@ -553,10 +554,9 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
|
|
|
553
554
|
return (record.instanceId === deps.instanceId &&
|
|
554
555
|
(record.status === "active" || record.status === "starting"));
|
|
555
556
|
});
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
if (!deps.forceFreshUnnamed && priorTargets.length > 0) {
|
|
557
|
+
// Legacy callers without session identity may reuse the active instance target.
|
|
558
|
+
// Session-aware callers must resolve their exact Workspace binding instead.
|
|
559
|
+
if (deps.sessionId === undefined && !deps.forceFreshUnnamed && priorTargets.length > 0) {
|
|
560
560
|
const record = priorTargets[0];
|
|
561
561
|
deps.recordEvent("telegram", "Leader thread preserved after session lifecycle change", {
|
|
562
562
|
phase: "leader-thread-reused",
|
|
@@ -812,6 +812,9 @@ function normalizePendingProvision(value) {
|
|
|
812
812
|
...(record.status === "in-flight" || record.status === "ambiguous"
|
|
813
813
|
? { status: record.status }
|
|
814
814
|
: {}),
|
|
815
|
+
...(typeof record.workspaceBindingKey === "string"
|
|
816
|
+
? { workspaceBindingKey: record.workspaceBindingKey }
|
|
817
|
+
: {}),
|
|
815
818
|
...(typeof record.threadName === "string"
|
|
816
819
|
? { threadName: record.threadName }
|
|
817
820
|
: {}),
|
|
@@ -3599,6 +3602,12 @@ export function createTelegramTopicTargetProvisioner(deps) {
|
|
|
3599
3602
|
.listPendingProvisions()
|
|
3600
3603
|
.find((pending) => pending.profileKey === request.profileKey ||
|
|
3601
3604
|
pending.instanceId === request.instanceId);
|
|
3605
|
+
const pendingTarget = pendingForRequest?.target;
|
|
3606
|
+
if (pendingForRequest && request.workspaceBindingKey &&
|
|
3607
|
+
pendingForRequest.workspaceBindingKey !== request.workspaceBindingKey &&
|
|
3608
|
+
!(pendingTarget && deps.store.listWorkspaceBindings().some((binding) => binding.bindingKey === request.workspaceBindingKey && targetMatches(binding.target, pendingTarget)))) {
|
|
3609
|
+
throw new Error("Telegram unfinished Thread creation does not match this session binding.");
|
|
3610
|
+
}
|
|
3602
3611
|
if (pendingForRequest?.target) {
|
|
3603
3612
|
const target = pendingForRequest.target;
|
|
3604
3613
|
const observation = deps.store.listSyncObservations().find((entry) => targetMatches(entry.target, target));
|
|
@@ -3611,8 +3620,11 @@ export function createTelegramTopicTargetProvisioner(deps) {
|
|
|
3611
3620
|
pendingForRequest = undefined;
|
|
3612
3621
|
}
|
|
3613
3622
|
}
|
|
3623
|
+
const matchesWorkspace = (target) => !request.workspaceBindingKey ||
|
|
3624
|
+
deps.store.listWorkspaceBindings().some((binding) => binding.bindingKey === request.workspaceBindingKey && targetMatches(binding.target, target)) ||
|
|
3625
|
+
(!!pendingForRequest?.target && targetMatches(pendingForRequest.target, target));
|
|
3614
3626
|
const existing = deps.store.getByProfileKey(request.profileKey);
|
|
3615
|
-
if (existing && isCurrentThreadRecord(existing)) {
|
|
3627
|
+
if (existing && isCurrentThreadRecord(existing) && matchesWorkspace(existing.target)) {
|
|
3616
3628
|
const slot = existing.slot ?? deps.store.allocateSlot(request.profileKey);
|
|
3617
3629
|
if (!slot) {
|
|
3618
3630
|
throw new TelegramWorkspaceSlotUnavailableError();
|
|
@@ -3691,7 +3703,7 @@ export function createTelegramTopicTargetProvisioner(deps) {
|
|
|
3691
3703
|
throw new Error(`Telegram topic provisioning remains ${pendingForRequest.status ?? "in-flight"} for this instance.`);
|
|
3692
3704
|
}
|
|
3693
3705
|
const activeForInstance = deps.store.getActiveByInstanceId(request.instanceId);
|
|
3694
|
-
if (activeForInstance) {
|
|
3706
|
+
if (activeForInstance && matchesWorkspace(activeForInstance.target)) {
|
|
3695
3707
|
if (!activeForInstance.slot) {
|
|
3696
3708
|
throw new Error("Telegram Workspace slot reservation is unavailable.");
|
|
3697
3709
|
}
|
|
@@ -3723,12 +3735,12 @@ export function createTelegramTopicTargetProvisioner(deps) {
|
|
|
3723
3735
|
const preferredNameSlot = getTelegramThreadNameLeadingSlot(candidateThreadName) ??
|
|
3724
3736
|
getNextTelegramThreadNamePaletteSlot(deps.store.list(), undefined) ??
|
|
3725
3737
|
request.preferredSlot;
|
|
3726
|
-
const slot = existing
|
|
3738
|
+
const slot = (existing && matchesWorkspace(existing.target) ? existing.slot : undefined) ??
|
|
3727
3739
|
deps.store.allocateSlot(request.profileKey, isManualFollowerRequest
|
|
3728
3740
|
? request.preferredSlot
|
|
3729
3741
|
: (request.preferredSlot ??
|
|
3730
3742
|
(candidateThreadName ? undefined : identity?.slot) ??
|
|
3731
|
-
preferredNameSlot), request.workspaceBindingKey);
|
|
3743
|
+
preferredNameSlot), request.workspaceBindingKey, { excludeCurrentRecord: !!existing && !matchesWorkspace(existing.target) });
|
|
3732
3744
|
if (!slot) {
|
|
3733
3745
|
throw new TelegramWorkspaceSlotUnavailableError();
|
|
3734
3746
|
}
|
|
@@ -3762,6 +3774,7 @@ export function createTelegramTopicTargetProvisioner(deps) {
|
|
|
3762
3774
|
owner: pendingOwner,
|
|
3763
3775
|
instanceId: request.instanceId,
|
|
3764
3776
|
profileKey: request.profileKey,
|
|
3777
|
+
...(request.workspaceBindingKey ? { workspaceBindingKey: request.workspaceBindingKey } : {}),
|
|
3765
3778
|
threadName: requestThreadName,
|
|
3766
3779
|
...(displayTitle ? { displayTitle } : {}),
|
|
3767
3780
|
slot,
|
|
@@ -218,7 +218,7 @@ export type TelegramWorkspaceRetirementExecution = {
|
|
|
218
218
|
};
|
|
219
219
|
export type TelegramWorkspaceRetirementAbsence = "absent" | "present" | "unknown";
|
|
220
220
|
export declare function executeTelegramWorkspaceRetirement(input: {
|
|
221
|
-
store: Pick<TelegramTopicTargetStore, "captureWorkspaceSlotOccupancy" | "listWorkspaceBindings" | "listWorkspaceRetirementIntents" | "commitWorkspaceRetirement" | "removeWorkspaceRetirementIntent" | "persist">;
|
|
221
|
+
store: Pick<TelegramTopicTargetStore, "captureWorkspaceSlotOccupancy" | "listWorkspaceBindings" | "listWorkspaceRetirementIntents" | "commitWorkspaceRetirement" | "removeWorkspaceRetirementIntent" | "markStaleByTarget" | "persist">;
|
|
222
222
|
admission: Pick<TelegramWorkspaceAdmissionLedger, "getOwner" | "read" | "acquireRetirementFence" | "adoptRetirementFence" | "issueDeletionPermit" | "confirmRetirementAbsence" | "confirmRetirementRejection" | "completeRejectedRetirementFence" | "releaseUnissuedRetirementFence" | "completeRetirementFence">;
|
|
223
223
|
intent: TelegramWorkspaceRetirementIntent;
|
|
224
224
|
getExternalProtection: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceExternalProtectionEvidence;
|
|
@@ -263,7 +263,7 @@ export type TelegramWorkspaceRetirementLifecycleResult = TelegramWorkspaceRetire
|
|
|
263
263
|
reason: string;
|
|
264
264
|
};
|
|
265
265
|
export declare function runTelegramWorkspaceRetirementLifecycle(input: {
|
|
266
|
-
store: TelegramWorkspaceRetirementPreparationDeps["store"] & Pick<TelegramTopicTargetStore, "replaceWorkspaceRetirementIntent" | "commitWorkspaceRetirement">;
|
|
266
|
+
store: TelegramWorkspaceRetirementPreparationDeps["store"] & Pick<TelegramTopicTargetStore, "replaceWorkspaceRetirementIntent" | "commitWorkspaceRetirement" | "markStaleByTarget">;
|
|
267
267
|
getExternalProtection: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceExternalProtectionEvidence;
|
|
268
268
|
getLeaderEpoch: () => number | string | undefined;
|
|
269
269
|
getProfileKey: () => string;
|
|
@@ -605,6 +605,12 @@ export async function executeTelegramWorkspaceRetirement(input) {
|
|
|
605
605
|
}
|
|
606
606
|
return { kind: "retained", reason: "stale-intent" };
|
|
607
607
|
}
|
|
608
|
+
if (fence?.phase === "commit-ready") {
|
|
609
|
+
// Exact confirmed absence retires any same-target routing record that
|
|
610
|
+
// survived the deletion attempt. The fence prevents replacement target
|
|
611
|
+
// publication while this stale local projection is removed.
|
|
612
|
+
input.store.markStaleByTarget(binding.target, "deleted");
|
|
613
|
+
}
|
|
608
614
|
const eligible = () => input.store.captureWorkspaceSlotOccupancy(input.getExternalProtection, { expectedRetirement: input.intent }).bindings.find((candidate) => candidate.bindingKey === input.intent.binding.bindingKey)?.protection === "eligible";
|
|
609
615
|
if (!eligible())
|
|
610
616
|
return { kind: "retained", reason: "protection-changed" };
|
|
@@ -669,6 +675,9 @@ export async function executeTelegramWorkspaceRetirement(input) {
|
|
|
669
675
|
}
|
|
670
676
|
if (!isCurrent())
|
|
671
677
|
return { kind: "retained", reason: "authority-changed" };
|
|
678
|
+
if (fence.phase === "commit-ready") {
|
|
679
|
+
input.store.markStaleByTarget(binding.target, "deleted");
|
|
680
|
+
}
|
|
672
681
|
if (!eligible())
|
|
673
682
|
return { kind: "retained", reason: "protection-changed" };
|
|
674
683
|
if (!await input.store.commitWorkspaceRetirement(input.intent, isCurrent)) {
|
|
@@ -799,19 +808,46 @@ export async function runTelegramWorkspaceRetirementLifecycle(input) {
|
|
|
799
808
|
});
|
|
800
809
|
}
|
|
801
810
|
if (intent) {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
intent
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
811
|
+
if (retainedFence && isTelegramWorkspaceRetirementFence(retainedFence) &&
|
|
812
|
+
retainedFence.phase === "commit-ready" &&
|
|
813
|
+
matchesTelegramWorkspaceRetirementFence(retainedFence, intent)) {
|
|
814
|
+
const resumed = await input.runExclusive(async () => {
|
|
815
|
+
const epoch = input.getLeaderEpoch();
|
|
816
|
+
const profileKey = input.getProfileKey();
|
|
817
|
+
const isCurrent = () => epoch !== undefined && input.getLeaderEpoch() === epoch &&
|
|
818
|
+
input.getProfileKey() === profileKey && input.isCurrent?.() !== false;
|
|
819
|
+
if (!isCurrent() || intent.profileKey !== profileKey)
|
|
820
|
+
return undefined;
|
|
821
|
+
const intents = input.store.listWorkspaceRetirementIntents();
|
|
822
|
+
const binding = input.store.listWorkspaceBindings().find(candidate => candidate.bindingKey === intent.binding.bindingKey);
|
|
823
|
+
if (intents.length !== 1 || !isDeepStrictEqual(intents[0], intent) ||
|
|
824
|
+
!binding || !isDeepStrictEqual(binding, intent.binding))
|
|
825
|
+
return undefined;
|
|
826
|
+
if (intent.leaderEpoch === epoch)
|
|
827
|
+
return intent;
|
|
828
|
+
const replacement = { ...intent, leaderEpoch: epoch };
|
|
829
|
+
return await input.store.replaceWorkspaceRetirementIntent(intent, replacement, isCurrent)
|
|
830
|
+
? replacement : undefined;
|
|
831
|
+
});
|
|
832
|
+
if (!resumed)
|
|
833
|
+
return { kind: "blocked", stage: "adoption", reason: "commit-rejected" };
|
|
834
|
+
intent = resumed;
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
const adoption = await adoptTelegramWorkspaceRetirementIntent({
|
|
838
|
+
store: input.store,
|
|
839
|
+
intent,
|
|
840
|
+
getExternalProtection: input.getExternalProtection,
|
|
841
|
+
getLeaderEpoch: input.getLeaderEpoch,
|
|
842
|
+
getProfileKey: input.getProfileKey,
|
|
843
|
+
isCurrent: input.isCurrent,
|
|
844
|
+
runExclusive: input.runExclusive,
|
|
845
|
+
});
|
|
846
|
+
if (adoption.kind !== "adopted") {
|
|
847
|
+
return { kind: "blocked", stage: "adoption", reason: adoption.reason };
|
|
848
|
+
}
|
|
849
|
+
intent = adoption.intent;
|
|
813
850
|
}
|
|
814
|
-
intent = adoption.intent;
|
|
815
851
|
}
|
|
816
852
|
else {
|
|
817
853
|
const preparation = await prepareTelegramWorkspaceRetirement({
|
|
@@ -42,7 +42,7 @@ Keep this boundary explicit:
|
|
|
42
42
|
|
|
43
43
|
### Instance, Session, And Context Cost
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Live Telegram routing belongs to the active authenticated Pi instance, while a durable Workspace Thread belongs to the exact profile/CWD/session identity. Ordinary Telegram prompts enter the session currently bound to that target. Local `/resume` replaces session-scoped runtime state and selects the destination session's binding; it never inherits the source Thread merely because the process is unchanged. Telegram exposes compaction and new-session replacement for the active session: `/new` first completes and removes its exact durable update, then dispatches an internal registered Pi command through `pi.sendUserMessage(..., { expandPromptTemplates: true })` so the handler runs with a real `ExtensionCommandContext` and calls the same `AgentSessionRuntime.newSession()` path as the terminal. Before invoking that path, the current transport owner CAS-publishes one expiring replacement intent inside the existing profile target snapshot. The intent discriminates `workspace-thread` from `classic-chat`. A same- or cross-process successor may consume either only for the exact profile, CWD, source session, target, and fresh lifetime: Threaded continuity re-keys the retained Workspace binding and preserves Thread, slot, and display identity, while Classic continuity preserves the exact chat target without creating a Workspace binding or entering topic lifecycle APIs. A confirmed callback is acknowledged and its dialog is deleted before session replacement begins. Once the successor has delivery authority, it atomically claims the intent by CAS-clearing it and then sends one separate terminal success result with bounded in-process transport retry. The old `withSession` path never publishes success, and an unclaimed intent never sends, preventing duplicate terminal notices across same-process startup and cleanup-ack ambiguity. Mismatch or expiry never authorizes binding takeover. Resume, fork, tree navigation, session switching, and full reload remain outside the stable Telegram API until Pi exposes safe public extension hooks for them.
|
|
46
46
|
|
|
47
47
|
`/telegram-connect` never launches a hidden or headless Pi process. A long-lived background Pi process can own Telegram only when something else explicitly launched that process and it satisfies the normal lock/runtime rules. Pi `print` and `json` modes stay passive and exit rather than becoming hidden polling owners.
|
|
48
48
|
|
|
@@ -90,8 +90,8 @@ The repository uses a **Flat Domain DAG**:
|
|
|
90
90
|
- `generative-apps`: managed deterministic application identity, canonical installation and explicit replacement, content-addressed module loading, state timelines, cross-process transition serialization, bounded executable-plus-argv adaptation, `telegram_bind`, and pre-model-queue `app::method` invocation. It does not own Telegram transport, arbitrary shell execution, or the external application adapted by one Generative App.
|
|
91
91
|
- `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, ordinary photo/document delivery, and narrow single-artifact Rich Message planning/sending for probe-confirmed photo/video/audio formats. It owns known-failure fallback eligibility and ambiguous-send no-replay classification through structural error contracts without importing Bot API helpers.
|
|
92
92
|
- `channel-posts`: profile/token-bound authority for agent-authored channel publication intents, one-shot send/edit/delete fencing, outcome-unknown retention, exact successful post identity, capacity refusal, and bounded local listing. It never reads Telegram history; the composition root supplies direct-leader Bot API effects only after its durable grants.
|
|
93
|
-
- `status` / `logs`: status bar/status-message rendering, queue-lane summaries, the structural redacted event ring, profile-aware JSONL scope/reset/append behavior, exact-owner destructive commits, fail-soft synchronous and queued diagnostics persistence, status snapshot scheduling, and
|
|
94
|
-
- `bindings` / `lifecycle` / `prompts` / `prompt-templates` / `pi`: Pi-facing command/tool/hook registration and cohesive cross-domain binding assembly, including queue mutation/dispatch/watchdog composition over admission and transport ports; session-generation fencing and start/shutdown sequencing across Queue, grouped input, Delivery, polling, capability monitor, follower refresh, and assistant-output projection; Telegram prompt guidance; prompt-template discovery/expansion; and centralized direct Pi SDK imports.
|
|
93
|
+
- `status` / `logs`: status bar/status-message rendering, queue-lane summaries, the structural redacted event ring, profile-aware JSONL scope/reset/append behavior, exact-owner destructive commits, fail-soft synchronous and queued diagnostics persistence, status snapshot scheduling, grouped diagnostics, and allowlisted compact connection-failure copy shared by commands and lifecycle. `status` remains a structural leaf; `logs` composes filesystem evidence with status projections and contains every persistence failure so diagnostics cannot terminate or poison the runtime queue.
|
|
94
|
+
- `bindings` / `lifecycle` / `prompts` / `prompt-templates` / `pi`: Pi-facing command/tool/hook registration and cohesive cross-domain binding assembly, including queue mutation/dispatch/watchdog composition over admission and transport ports; session-generation fencing and start/shutdown sequencing across Queue, grouped input, Delivery, polling, capability monitor, follower refresh, and assistant-output projection; Telegram prompt guidance; prompt-template discovery/expansion; and centralized direct Pi SDK imports. `lifecycle` also owns bounded same-process connect intent across local `/resume`, cancellation/supersession, and fresh-context startup scheduling; it does not own transport authority, durable bindings, queue custody, or source-target continuity.
|
|
95
95
|
- `command-templates`: shell-free command-template helpers, composition expansion, placeholder substitution, executable resolution, warnings, and retry/timeout semantics.
|
|
96
96
|
|
|
97
97
|
### Host Compatibility Boundary
|
|
@@ -326,14 +326,15 @@ Read-only migration probes constrain that comparison. Publishing a v2 snapshot o
|
|
|
326
326
|
### Runtime Ownership
|
|
327
327
|
|
|
328
328
|
- `/telegram-connect` acquires or moves the active profile's owner slot before polling starts. `/telegram-disconnect` keeps its destructive confirmation, then stops polling and releases only that exact slot. In Threaded Mode it tears down the disconnecting instance's bound Telegram thread: leaders delete their own thread directly, and followers send an authenticated exact-generation disconnect envelope and wait for confirmed leader cleanup before unregistering. Graceful Pi `quit` always preserves the owner slot as restart intent, allowing a reopened same-`cwd` session to reclaim the stale lease. When `threads.automaticCleanup` is enabled (the default), quit also deletes the bound Telegram tab without releasing that restart intent; disabling it preserves the tab. After delivery, polling and inbound-worker shutdown complete, a quitting leader detaches its exact owner record under profile admission while retaining the Workspace, letter and restart ownership. Publication rechecks the captured session generation, profile, leader epoch and successful current-generation suspension; unfinished startup/teardown, unproven suspension and pending cleanup stay protected. Preparation/publication errors are diagnostic and cannot block remaining session cleanup. Failed automatic deletion likewise falls back to safe suspension.
|
|
329
|
-
-
|
|
329
|
+
- Without an explicit carried connection intent, session start schedules polling resume asynchronously only when the owner slot already points at the current `pid`/`cwd`, or when a stale same-`cwd` owner can be safely replaced after process restart. Under a live foreign leader, startup instead attempts capability-gated restore-only follower admission when the selected profile has a remembered exact-`cwd` Workspace binding; it never provisions an unremembered Workspace. Startup and `/resume` do not wait on leader election, Bot API probes, poller handoff, or thread reconciliation before restoring the Pi session.
|
|
330
|
+
- On local `/resume`, an in-flight `/telegram-connect` or established connection supplies one process-local, 30-second intent containing the selected profile, exact CWD and target session file. The file matches only the lifecycle successor; the fresh public session ID still owns binding selection. Startup claims intent before asynchronous initialization and uses the normal connection path instead of concurrent auto-restore. Quit/reload/disconnect, mismatch, expiry or supersession cancel it. The predecessor carries no Pi context, callback, Thread or slot, and late completion can clear only its own attempt. Resume startup has no automatic forced takeover or repeated failure retry; errors remain diagnostic with a compact recovery notice.
|
|
330
331
|
- The polling owner alone bounds `getUpdates`: each request derives its cancellation budget from Telegram's declared long-poll timeout plus 10 seconds of transport grace (10 seconds for the zero-timeout initial sync and 40 seconds for the normal 30-second poll). The request-local controller inherits poller cancellation, rejects its owner at the budget, and fences any late transport result. Ordinary Bot API and media operations do not receive speculative blanket deadlines. Existing caller signals remain authoritative through API retry waits, only retry-safe methods replay explicit retryable responses, and non-idempotent sends preserve commit-unknown evidence instead of risking duplicate mutation.
|
|
331
332
|
- Ten consecutive `getUpdates` conflict responses, including initial cursor sync, terminate polling with `persistent-conflict`; a successful response or a different error resets the count. The controller detaches its inner promise before notifying the locked lifecycle, avoiding teardown waiting on itself. That lifecycle stops ownership checks, lease refresh, capability monitoring, typing, and classic or bus transport (including leader health, pruning, and IPC), withdraws local direct authority, and transactionally releases only its exact lock. A failed durable release leaves local authority revoked; explicit reacquisition mints a fresh epoch. Accepted queue receipts and local Pi dispatch survive. One terminal diagnostic distinguishes lost local ownership from a competing client despite an apparently owned lock and reports cleanup failures; ordinary status refreshes retain generic `error` until transport recovers. Remove the competing client, then use `/telegram-connect`; no automatic retry continues after the terminal threshold.
|
|
332
333
|
- Manual and automatic polling starts share a lifecycle generation. A later suspend, disconnect, persistent-conflict stop, or accepted start invalidates older startup continuations; reconnect captures its generation before waiting for transport teardown and rechecks it before acquiring ownership and after awaited startup work. Obsolete completion or failure cannot report a successful connection or roll back a replacement. Admission rejects stale/unauthorized contexts before advancing its own startup generation, so a rejected call cannot cancel valid in-flight initialization. After awaited bus startup, thread-aware completion checks its generation before starting leader health or changing fallback/startup-option state; teardown clears health independently of the current mode flag. Startup probes, capability-monitor transitions, and observed-target transitions share an orchestration lifecycle fence and check it inside their effect-owning helpers after awaited queries, persistence, or transport work. Monitor stop also invalidates pending observations. These checks suppress subsequent state changes, fallback, health, and status effects; already-issued API/persistence calls retain their own transport/storage fencing.
|
|
333
334
|
- `pollingActive` reports only whether this runtime still owns an unresolved polling lifecycle; it is not health evidence. A separate observable state records `starting`, `long-poll`, `persisting-journal`, `persisting-offset`, `retrying`, or `stopped`, together with phase start, current update id, last successful response time/count, and terminal stop reason. This distinguishes a stuck HTTP poll from downstream update work without a wall-clock stale heuristic.
|
|
334
335
|
- Built-in read-only menu commands return after required local mutation and schedule context-fenced rendering and command synchronization independently, so those effects cannot withhold the next inbound offset.
|
|
335
336
|
- Pi `print`/`json` run modes stay passive. Inherited child sessions that share `telegram.json` but do not own the exact `pid`/`cwd` slot must not poll or call `getUpdates` unless the operator force-takes ownership.
|
|
336
|
-
- Session replacement through `reload`, `new`, `resume`, or `fork` suspends polling/watchers without releasing ownership or publishing inactivity so the next session in the same process can resume. Late shutdown cannot clear a replacement context even when its identity is reused: the captured session generation must still match.
|
|
337
|
+
- Session replacement through `reload`, `new`, `resume`, or `fork` suspends polling/watchers without releasing ownership or publishing inactivity so the next session in the same process can resume. Late shutdown cannot clear a replacement context even when its identity is reused: the captured session generation must still match. For replacements other than `/resume`, a registered follower snapshots its assigned target into a short-lived same-process handoff, stops the old receiver/heartbeat, and re-registers through the live leader without marking or replacing its Telegram thread. Local `/resume` suppresses that source-target handoff and selects the destination session's binding instead. Hard process termination cannot run graceful teardown, so stale recovery retains its restart-hint path.
|
|
337
338
|
- Live external owners require explicit takeover confirmation. Long-lived timers compare against snapshotted owner identity and stop local transport work when the slot no longer matches.
|
|
338
339
|
- `owners.json` owns only Telegram transport control. Local extension and accepted queue state remain per Pi instance when ownership moves, but previews, final delivery, dispatch transport mutations, and delayed Bot API work fail closed until exact direct or follower authority becomes valid again.
|
|
339
340
|
- Exact ownership remains checked every second, while the durable owner heartbeat refresh runs every two seconds and becomes stale after eight seconds. This keeps replacement detection responsive while halving steady-state atomic `owners.json` rewrites without changing the cross-platform file-transaction authority. Every acquisition, refresh, release, takeover, and stale recovery serializes through the sibling `owners.json.transaction` guard. The guard publishes one private generation-named owner record atomically, validates filename/payload generation agreement, fences stale recovery and delayed release against replacement-owner ABA, and fails closed on malformed state, unverifiable ownership, contention timeout, or unsupported filesystem behavior. The JSON store publishes through a private same-directory temporary file and atomic rename; atomic payload replacement does not replace transaction serialization.
|
|
@@ -208,6 +208,8 @@ The persisted binding stores the exact session ID and derives its new binding ke
|
|
|
208
208
|
|
|
209
209
|
Legacy cwd-only bindings remain exact compatibility identities whose absent session component is itself a unique key. They are neither wildcard session bindings nor adoption candidates: session-aware claims never stamp, migrate, consume, or restore them, and instead allocate their own `{ cwd, sessionId }` binding subject to the same global slot and protection rules. `{ cwd, ∅ }`, `{ cwd, session-a }`, and `{ cwd, session-b }` may coexist as majestic degradation for older state and peers. The 0.46 runtime reads strict legacy records as inert independent `{ cwd, ∅ }` bindings but never migrates, adopts, or routes a session through them. Protocol v2 is session-native and intentionally incompatible with protocol-v1 0.45.x peers. Upgrade requires stopping all Telegram instances first, updating them together, and then starting them again; the first v2 process becomes leader with its own session-qualified binding.
|
|
210
210
|
|
|
211
|
+
Local `/resume` suppresses the same-process source-target handoff. A carried explicit connect intent or established connection starts the destination through normal owner acquisition/follower registration before ordinary auto-restore; it restores that session's exact binding or provisions a new one. Pending Thread-creation evidence records its Workspace binding key. Recovery rejects a different or unproven session key without consuming evidence or repeating creation; legacy pending evidence is reusable only when its target already proves the exact requested binding. Profile/CWD equality and a shared PID never authorize session reassignment. Telegram `/new` remains the separately authenticated, durable continuity exception described above.
|
|
212
|
+
|
|
211
213
|
Routing authority remains the authenticated live owner plus exact numeric Telegram target. Session identity selects and restores a durable binding but never authorizes message delivery by itself. Slot pressure and retirement continue to operate on exact binding snapshots; they must carry the session component so cleanup, displacement, and letter reuse cannot target another session in the same directory.
|
|
212
214
|
|
|
213
215
|
## Leader Election
|
|
@@ -40,8 +40,8 @@ import {
|
|
|
40
40
|
Stable commands inside Pi:
|
|
41
41
|
|
|
42
42
|
- `/telegram-setup` — configure/update the bot token.
|
|
43
|
-
- `/telegram-connect` — start polling here and acquire external Telegram control ownership, optionally for a named profile. Thread names are configured from Telegram rather than Pi commands. Accepted queue/reply state stays local if ownership later moves elsewhere. A successful command queues a hidden connection-state note for delivery with the agent's next turn without triggering one; it says Telegram is connected and that connectivity alone is not user intent.
|
|
44
|
-
- `/telegram-disconnect` — after destructive confirmation, stop polling and release ownership without deleting or silencing accepted local queue state. A successful command queues the corresponding hidden, non-triggering disconnected context note; cancelled or failed disconnects do not publish a false state transition. In Threaded Mode it deletes this instance's current Telegram thread; a follower waits for its active leader to confirm generation-fenced cleanup before stopping. Graceful Pi `quit` performs the same teardown without prompting, while
|
|
43
|
+
- `/telegram-connect` — start polling here and acquire external Telegram control ownership, optionally for a named profile. Thread names are configured from Telegram rather than Pi commands. Accepted queue/reply state stays local if ownership later moves elsewhere. A successful command queues a hidden connection-state note for delivery with the agent's next turn without triggering one; it says Telegram is connected and that connectivity alone is not user intent. An immediate local `/resume` carries pending connection intent and the selected profile to the destination session without carrying the source Thread; its own binding is restored or allocated through normal authority checks. Connection failures produce one compact cause/action notice instead of rethrowing raw exception text into Pi; redacted technical evidence remains available through `/telegram-status --debug`.
|
|
44
|
+
- `/telegram-disconnect` — after destructive confirmation, stop polling and release ownership without deleting or silencing accepted local queue state. A successful command queues the corresponding hidden, non-triggering disconnected context note; cancelled or failed disconnects do not publish a false state transition. In Threaded Mode it deletes this instance's current Telegram thread; a follower waits for its active leader to confirm generation-fenced cleanup before stopping. Graceful Pi `quit` performs the same teardown without prompting, while session replacement suspends transport without disconnect teardown. Local `/resume` carries connection intent rather than the source Thread; other supported replacements retain their existing continuity rules.
|
|
45
45
|
- `/telegram-status` — show connection, polling, execution, queue, and recent event diagnostics; debug output separates poller and worker progress, durable automatic-retry state, exact foreign queued-owner identity, and negotiated protocol/build/capabilities.
|
|
46
46
|
|
|
47
47
|
### Telegram commands
|
|
@@ -218,6 +218,10 @@ Examples:
|
|
|
218
218
|
- Main menu → Settings: first row is `⬆️ Main menu`.
|
|
219
219
|
- Settings → Voice reply mode: first row is `⬆️ Back`.
|
|
220
220
|
|
|
221
|
+
## Pi Connection Notices
|
|
222
|
+
|
|
223
|
+
Pi TUI connection notices use plain text: a short known cause and one recovery action. Unknown failures use a generic connection-failed notice with `/telegram-status --debug`; never interpolate raw exceptions, credentials, stack traces or Pi lifecycle guidance. Technical evidence belongs in the redacted runtime recorder. Failed disconnect must retain the instruction to keep Pi open; do not also rethrow the same error as a second Pi banner. These TUI notices are distinct from Telegram bot message cards below.
|
|
224
|
+
|
|
221
225
|
## Message Cards
|
|
222
226
|
|
|
223
227
|
Message cards and standalone informational notices sent by the bot should start with a strong heading.
|
|
@@ -602,6 +602,9 @@ interface TelegramCommandsAndToolsBindingDeps {
|
|
|
602
602
|
| undefined,
|
|
603
603
|
) => void;
|
|
604
604
|
updateStatus: TelegramBridgeStatusUpdater;
|
|
605
|
+
isContextCurrent: (ctx: Pi.ExtensionContext) => boolean;
|
|
606
|
+
getSessionGeneration: () => number;
|
|
607
|
+
connectionIntent: NonNullable<Commands.TelegramBridgeCommandRegistrationDeps["connectionIntent"]>;
|
|
605
608
|
recordRuntimeEvent: TelegramRuntimeEventRecorder;
|
|
606
609
|
}
|
|
607
610
|
|
|
@@ -633,6 +636,9 @@ export function registerTelegramCommandsAndTools({
|
|
|
633
636
|
setGenerativeAppLiveSurfaceRuntime,
|
|
634
637
|
recordRuntimeEvent,
|
|
635
638
|
updateStatus,
|
|
639
|
+
isContextCurrent,
|
|
640
|
+
getSessionGeneration,
|
|
641
|
+
connectionIntent,
|
|
636
642
|
}: TelegramCommandsAndToolsBindingDeps): void {
|
|
637
643
|
GenerativeApps.registerTelegramBindTool(pi, {
|
|
638
644
|
agentDir,
|
|
@@ -783,38 +789,39 @@ export function registerTelegramCommandsAndTools({
|
|
|
783
789
|
reloadConfig: configStore.load,
|
|
784
790
|
hasBotToken: configStore.hasBotToken,
|
|
785
791
|
getBotTokenDiagnostic: configStore.getBotTokenDiagnostic,
|
|
786
|
-
startPolling:
|
|
787
|
-
|
|
788
|
-
return await lockedPollingRuntime.start(ctx, options);
|
|
789
|
-
} catch (error) {
|
|
790
|
-
recordRuntimeEvent("recovery", error, { phase: "polling-start" });
|
|
791
|
-
throw error;
|
|
792
|
-
}
|
|
793
|
-
},
|
|
792
|
+
startPolling: lockedPollingRuntime.start,
|
|
793
|
+
recordConnectionEvent: (error, phase) => recordRuntimeEvent("connection", error, { phase }),
|
|
794
794
|
stopPolling: stopPolling ?? lockedPollingRuntime.stop,
|
|
795
795
|
recoverPollingStart,
|
|
796
796
|
getDisconnectThreadName,
|
|
797
797
|
queueAgentConnectionContext,
|
|
798
798
|
updateStatus,
|
|
799
|
+
isContextCurrent,
|
|
800
|
+
getSessionGeneration,
|
|
801
|
+
connectionIntent,
|
|
799
802
|
getProfileNames: () =>
|
|
800
803
|
Config.getTelegramProfileNames(configStore.getStoredConfig()),
|
|
801
|
-
activateDefaultProfileConfig: async () => {
|
|
804
|
+
activateDefaultProfileConfig: async (_ctx, isCurrent) => {
|
|
802
805
|
const previousProfileName = configStore.getActiveProfileName();
|
|
803
806
|
await configStore.load();
|
|
807
|
+
if (!isCurrent()) return;
|
|
804
808
|
if (previousProfileName) {
|
|
805
809
|
await (stopPolling ?? lockedPollingRuntime.stop)();
|
|
810
|
+
if (!isCurrent()) return;
|
|
806
811
|
}
|
|
807
812
|
configStore.activateProfile(undefined);
|
|
808
813
|
await onTransportChanged?.();
|
|
809
814
|
},
|
|
810
|
-
activateProfileConfig: async (_ctx, profileName) => {
|
|
815
|
+
activateProfileConfig: async (_ctx, profileName, isCurrent) => {
|
|
811
816
|
const previousProfileName = configStore.getActiveProfileName();
|
|
812
817
|
await configStore.load();
|
|
818
|
+
if (!isCurrent()) return false;
|
|
813
819
|
if (!Config.isValidTelegramProfileName(profileName)) return false;
|
|
814
820
|
const storedConfig = configStore.getStoredConfig();
|
|
815
821
|
if (!storedConfig.profiles?.[profileName]) return false;
|
|
816
822
|
if (previousProfileName !== profileName) {
|
|
817
823
|
await (stopPolling ?? lockedPollingRuntime.stop)();
|
|
824
|
+
if (!isCurrent()) return false;
|
|
818
825
|
}
|
|
819
826
|
if (!configStore.activateProfile(profileName)) return false;
|
|
820
827
|
await onTransportChanged?.();
|
|
@@ -1130,10 +1130,16 @@ function isTelegramStaleContextError(error: unknown): boolean {
|
|
|
1130
1130
|
|
|
1131
1131
|
export function createTelegramBusFollowerSessionReplacementSuspender(
|
|
1132
1132
|
deps: TelegramBusFollowerSessionReplacementSuspenderDeps,
|
|
1133
|
-
): () => Promise<void> {
|
|
1133
|
+
): (preserveTarget?: boolean) => Promise<void> {
|
|
1134
1134
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1135
1135
|
const getPid = deps.getPid ?? (() => process.pid);
|
|
1136
|
-
return async () => {
|
|
1136
|
+
return async (preserveTarget = true) => {
|
|
1137
|
+
if (!preserveTarget) {
|
|
1138
|
+
setTelegramFollowerSessionHandoff(undefined);
|
|
1139
|
+
Threads.setTelegramLeaderSessionHandoff(undefined);
|
|
1140
|
+
await deps.suspendPolling();
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1137
1143
|
const target = deps.registrationState.getTarget();
|
|
1138
1144
|
if (deps.registrationState.isRegistered() && target) {
|
|
1139
1145
|
setTelegramFollowerSessionHandoff({
|