@llblab/pi-telegram 0.46.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -2
- package/BACKLOG.md +0 -6
- package/CHANGELOG.md +14 -0
- package/README.md +5 -2
- package/dist/api/activity.d.ts +6 -0
- package/dist/api/activity.js +6 -0
- package/dist/api/commands.d.ts +6 -0
- package/dist/api/commands.js +6 -0
- package/dist/api/delivery.d.ts +6 -0
- package/dist/api/delivery.js +6 -0
- package/dist/api/inbound.d.ts +6 -0
- package/dist/api/inbound.js +6 -0
- package/dist/api/keyboard.d.ts +6 -0
- package/dist/api/keyboard.js +6 -0
- package/dist/api/outbound.d.ts +6 -0
- package/dist/api/outbound.js +6 -0
- package/dist/api/sections.d.ts +7 -0
- package/dist/api/sections.js +6 -0
- package/dist/api/status.d.ts +6 -0
- package/dist/api/status.js +6 -0
- package/dist/api/updates.d.ts +6 -0
- package/dist/api/updates.js +6 -0
- package/dist/api/voice.d.ts +6 -0
- package/dist/api/voice.js +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/activity-verbosity.d.ts +52 -0
- package/dist/lib/activity-verbosity.js +596 -0
- package/dist/lib/activity.d.ts +220 -0
- package/dist/lib/activity.js +574 -0
- package/dist/lib/agent-messages.d.ts +28 -0
- package/dist/lib/agent-messages.js +86 -0
- package/dist/lib/bindings.d.ts +250 -0
- package/dist/lib/bindings.js +936 -0
- package/dist/lib/bus-api.d.ts +18 -0
- package/dist/lib/bus-api.js +254 -0
- package/dist/lib/bus-follower.d.ts +464 -0
- package/dist/lib/bus-follower.js +1686 -0
- package/dist/lib/bus-leader.d.ts +292 -0
- package/dist/lib/bus-leader.js +2242 -0
- package/dist/lib/bus-transport.d.ts +64 -0
- package/dist/lib/bus-transport.js +140 -0
- package/dist/lib/bus.d.ts +518 -0
- package/dist/lib/bus.js +2055 -0
- package/dist/lib/channel-posts.d.ts +170 -0
- package/dist/lib/channel-posts.js +611 -0
- package/dist/lib/command-templates.d.ts +70 -0
- package/dist/lib/command-templates.js +744 -0
- package/dist/lib/commands.d.ts +541 -0
- package/dist/lib/commands.js +1155 -0
- package/dist/lib/config.d.ts +252 -0
- package/dist/lib/config.js +889 -0
- package/dist/lib/delivery.d.ts +163 -0
- package/dist/lib/delivery.js +542 -0
- package/dist/lib/extension.d.ts +7 -0
- package/dist/lib/extension.js +1604 -0
- package/dist/lib/generative-app-worker.mjs +104 -0
- package/dist/lib/generative-apps.d.ts +212 -0
- package/dist/lib/generative-apps.js +1006 -0
- package/dist/lib/inbound.d.ts +91 -0
- package/dist/lib/inbound.js +502 -0
- package/dist/lib/journal.d.ts +665 -0
- package/dist/lib/journal.js +3701 -0
- package/dist/lib/keyboard.d.ts +23 -0
- package/dist/lib/keyboard.js +37 -0
- package/dist/lib/lifecycle.d.ts +157 -0
- package/dist/lib/lifecycle.js +395 -0
- package/dist/lib/locks.d.ts +164 -0
- package/dist/lib/locks.js +1208 -0
- package/dist/lib/logging.d.ts +50 -0
- package/dist/lib/logging.js +274 -0
- package/dist/lib/media.d.ts +181 -0
- package/dist/lib/media.js +602 -0
- package/dist/lib/menu-model.d.ts +217 -0
- package/dist/lib/menu-model.js +663 -0
- package/dist/lib/menu-queue.d.ts +37 -0
- package/dist/lib/menu-queue.js +408 -0
- package/dist/lib/menu-settings.d.ts +115 -0
- package/dist/lib/menu-settings.js +595 -0
- package/dist/lib/menu-status.d.ts +32 -0
- package/dist/lib/menu-status.js +112 -0
- package/dist/lib/menu-thinking.d.ts +29 -0
- package/dist/lib/menu-thinking.js +82 -0
- package/dist/lib/menu.d.ts +171 -0
- package/dist/lib/menu.js +323 -0
- package/dist/lib/model.d.ts +127 -0
- package/dist/lib/model.js +409 -0
- package/dist/lib/outbound-attachments.d.ts +241 -0
- package/dist/lib/outbound-attachments.js +639 -0
- package/dist/lib/outbound-buttons.d.ts +69 -0
- package/dist/lib/outbound-buttons.js +248 -0
- package/dist/lib/outbound-markup.d.ts +42 -0
- package/dist/lib/outbound-markup.js +678 -0
- package/dist/lib/outbound-voice.d.ts +55 -0
- package/dist/lib/outbound-voice.js +152 -0
- package/dist/lib/outbound.d.ts +185 -0
- package/dist/lib/outbound.js +516 -0
- package/dist/lib/ownership.d.ts +77 -0
- package/dist/lib/ownership.js +174 -0
- package/dist/lib/paths.d.ts +40 -0
- package/dist/lib/paths.js +94 -0
- package/dist/lib/pi.d.ts +72 -0
- package/dist/lib/pi.js +121 -0
- package/dist/lib/polling.d.ts +351 -0
- package/dist/lib/polling.js +1196 -0
- package/dist/lib/preview.d.ts +192 -0
- package/dist/lib/preview.js +614 -0
- package/dist/lib/prompt-templates.d.ts +24 -0
- package/dist/lib/prompt-templates.js +118 -0
- package/dist/lib/prompts.d.ts +57 -0
- package/dist/lib/prompts.js +167 -0
- package/dist/lib/queue.d.ts +766 -0
- package/dist/lib/queue.js +1965 -0
- package/dist/lib/recovery.d.ts +86 -0
- package/dist/lib/recovery.js +285 -0
- package/dist/lib/rendering.d.ts +20 -0
- package/dist/lib/rendering.js +983 -0
- package/dist/lib/replies.d.ts +214 -0
- package/dist/lib/replies.js +737 -0
- package/dist/lib/routing.d.ts +207 -0
- package/dist/lib/routing.js +2282 -0
- package/dist/lib/runtime.d.ts +172 -0
- package/dist/lib/runtime.js +402 -0
- package/dist/lib/sections.d.ts +165 -0
- package/dist/lib/sections.js +327 -0
- package/dist/lib/setup.d.ts +82 -0
- package/dist/lib/setup.js +150 -0
- package/dist/lib/skills.d.ts +8 -0
- package/dist/lib/skills.js +12 -0
- package/dist/lib/status.d.ts +442 -0
- package/dist/lib/status.js +1008 -0
- package/dist/lib/sync.d.ts +179 -0
- package/dist/lib/sync.js +782 -0
- package/dist/lib/target.d.ts +20 -0
- package/dist/lib/target.js +27 -0
- package/dist/lib/telegram-api.d.ts +541 -0
- package/dist/lib/telegram-api.js +1159 -0
- package/dist/lib/text-groups.d.ts +89 -0
- package/dist/lib/text-groups.js +317 -0
- package/dist/lib/thread-cleanup-manager.d.ts +288 -0
- package/dist/lib/thread-cleanup-manager.js +560 -0
- package/dist/lib/thread-display.d.ts +46 -0
- package/dist/lib/thread-display.js +257 -0
- package/dist/lib/thread-naming.d.ts +46 -0
- package/dist/lib/thread-naming.js +78 -0
- package/dist/lib/thread-reconciler.d.ts +239 -0
- package/dist/lib/thread-reconciler.js +644 -0
- package/dist/lib/threads.d.ts +621 -0
- package/dist/lib/threads.js +3679 -0
- package/dist/lib/time-injection.d.ts +15 -0
- package/dist/lib/time-injection.js +56 -0
- package/dist/lib/turns.d.ts +109 -0
- package/dist/lib/turns.js +500 -0
- package/dist/lib/updates.d.ts +1290 -0
- package/dist/lib/updates.js +3634 -0
- package/dist/lib/voice.d.ts +117 -0
- package/dist/lib/voice.js +174 -0
- package/dist/lib/workspace-admission.d.ts +264 -0
- package/dist/lib/workspace-admission.js +1136 -0
- package/dist/lib/workspace-retirement.d.ts +219 -0
- package/dist/lib/workspace-retirement.js +587 -0
- package/dist/lib/workspace-slots.d.ts +30 -0
- package/dist/lib/workspace-slots.js +54 -0
- package/dist/package.json +126 -0
- package/dist/pi-telegram/index.js +1 -0
- package/dist/skills/generated-control-surface/SKILL.md +107 -0
- package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
- package/dist/skills/generative-apps/SKILL.md +115 -0
- package/dist/skills/show-me/SKILL.md +166 -0
- package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
- package/dist/skills/telegram-bridge/SKILL.md +129 -0
- package/dist/skills/telegram-bridge/references/configuration.md +15 -0
- package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
- package/docs/README.md +2 -0
- package/docs/architecture.md +1 -1
- package/docs/compact-matrix-literal.md +10 -3
- package/docs/generative-apps.md +15 -13
- package/docs/multi-instance-bus.md +6 -4
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +3 -3
- package/lib/bindings.ts +89 -6
- package/lib/bus-follower.ts +7 -3
- package/lib/bus-leader.ts +44 -19
- package/lib/bus.ts +4 -1
- package/lib/commands.ts +2 -2
- package/lib/config.ts +14 -7
- package/lib/delivery.ts +38 -6
- package/lib/extension.ts +7 -0
- package/lib/generative-apps.ts +379 -13
- package/lib/menu-settings.ts +20 -6
- package/lib/outbound-markup.ts +16 -7
- package/lib/status.ts +15 -12
- package/lib/telegram-api.ts +12 -1
- package/lib/thread-display.ts +96 -18
- package/package.json +56 -13
- package/scripts/build-dist.mjs +44 -0
- package/scripts/measure-bus.mjs +8 -1
- package/scripts/measure-workspace.mjs +8 -1
- package/skills/generative-apps/SKILL.md +1 -1
- package/skills/show-me/SKILL.md +1 -1
- package/skills/show-me/references/telegram-surfaces.md +1 -1
- package/skills/telegram-bridge/SKILL.md +1 -1
|
@@ -0,0 +1,2242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram bus leader orchestration
|
|
3
|
+
* Zones: multi-instance bus, leader polling/server lifecycle, follower routing
|
|
4
|
+
* Owns leader-only runtime orchestration: follower registration envelopes, follower API proxying,
|
|
5
|
+
* leader activation hot-switching, local bus server startup, and stale follower pruning.
|
|
6
|
+
*/
|
|
7
|
+
import * as Sync from "./sync.js";
|
|
8
|
+
import { createTelegramThreadDisplayReconciler, resolveTelegramInitialWorkspaceDisplayName, resolveTelegramLiveWorkspaceBindingKeys, } from "./thread-display.js";
|
|
9
|
+
import * as ThreadReconciler from "./thread-reconciler.js";
|
|
10
|
+
import { getTelegramApiErrorRequestTarget, isTelegramApiCommitUnknownError, } from "./telegram-api.js";
|
|
11
|
+
import * as Threads from "./threads.js";
|
|
12
|
+
import { createTelegramBusLocalServer, createUnauthorizedBusAck, getTelegramBusEnvelopeTrafficClass, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, getTelegramBusFollowerSocketPath, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, } from "./bus.js";
|
|
13
|
+
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.js";
|
|
14
|
+
import { createTelegramWorkspaceOperationRuntime, } from "./workspace-retirement.js";
|
|
15
|
+
import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
16
|
+
export const TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS = 15_000;
|
|
17
|
+
function escapeHtml(text) {
|
|
18
|
+
return text
|
|
19
|
+
.replace(/&/g, "&")
|
|
20
|
+
.replace(/</g, "<")
|
|
21
|
+
.replace(/>/g, ">");
|
|
22
|
+
}
|
|
23
|
+
function formatTelegramBusInstanceLabel(input) {
|
|
24
|
+
const threadName = input.threadName?.trim();
|
|
25
|
+
if (threadName)
|
|
26
|
+
return escapeHtml(threadName);
|
|
27
|
+
return input.slot && /^[A-Z]$/.test(input.slot) ? input.slot : "?";
|
|
28
|
+
}
|
|
29
|
+
export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
30
|
+
const runWorkspaceAdmission = deps.getWorkspaceAdmission
|
|
31
|
+
? (input, operation) => {
|
|
32
|
+
const admission = deps.getWorkspaceAdmission?.();
|
|
33
|
+
if (!admission) {
|
|
34
|
+
throw new Error("Telegram Workspace admission authority is unavailable.");
|
|
35
|
+
}
|
|
36
|
+
return runWithTelegramWorkspaceAdmissionsAsync({
|
|
37
|
+
ledger: admission,
|
|
38
|
+
...input,
|
|
39
|
+
operation,
|
|
40
|
+
onReleaseError(error) {
|
|
41
|
+
deps.recordRuntimeEvent("bus", error, {
|
|
42
|
+
phase: "workspace-admission-release",
|
|
43
|
+
operationKind: input.operationKind,
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
: undefined;
|
|
49
|
+
const runWorkspaceOperation = deps.runWorkspaceOperation ??
|
|
50
|
+
createTelegramWorkspaceOperationRuntime({
|
|
51
|
+
getWorkspaceAdmission: deps.getWorkspaceAdmission,
|
|
52
|
+
onReleaseError(error, operationKind) {
|
|
53
|
+
deps.recordRuntimeEvent("bus", error, {
|
|
54
|
+
phase: "workspace-admission-release",
|
|
55
|
+
operationKind,
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
}).run;
|
|
59
|
+
const captureLiveBindingKeys = (bindings) => resolveTelegramLiveWorkspaceBindingKeys(bindings, deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target, deps.runtime.followerRegistry.list());
|
|
60
|
+
const provisionerPorts = {
|
|
61
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
62
|
+
topicTargetStore: deps.topicTargetStore,
|
|
63
|
+
callApi: deps.callApi,
|
|
64
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
65
|
+
getSyncState: deps.getSyncState,
|
|
66
|
+
setSyncState: deps.setSyncState,
|
|
67
|
+
onProvisioningStart: deps.onProvisioningStart,
|
|
68
|
+
onProvisioningEnd: deps.onProvisioningEnd,
|
|
69
|
+
...(deps.getThreadDisplayMode
|
|
70
|
+
? {
|
|
71
|
+
resolveInitialWorkspaceDisplayTitle(binding) {
|
|
72
|
+
return resolveTelegramInitialWorkspaceDisplayName({
|
|
73
|
+
bindings: deps.topicTargetStore.listWorkspaceBindings(),
|
|
74
|
+
binding,
|
|
75
|
+
mode: deps.getThreadDisplayMode(),
|
|
76
|
+
liveBindingKeys: captureLiveBindingKeys(deps.topicTargetStore.listWorkspaceBindings()),
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
: {}),
|
|
81
|
+
runWorkspaceOperation,
|
|
82
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
83
|
+
};
|
|
84
|
+
const display = deps.getThreadDisplayMode ? createTelegramThreadDisplayReconciler({
|
|
85
|
+
store: deps.topicTargetStore,
|
|
86
|
+
getMode: deps.getThreadDisplayMode,
|
|
87
|
+
getProfileKey: () => deps.getTelegramProfile?.() ?? "default",
|
|
88
|
+
getLeaderEpoch: () => deps.getCurrentLeaderEpoch?.(),
|
|
89
|
+
captureLiveBindingKeys,
|
|
90
|
+
captureBindingAuthority(binding) {
|
|
91
|
+
const matches = (target) => target?.chatId === binding.target.chatId && target?.threadId === binding.target.threadId;
|
|
92
|
+
const leader = deps.topicTargetStore.getActiveByInstanceId(deps.instanceId);
|
|
93
|
+
if (leader && matches(leader.target)) {
|
|
94
|
+
return () => matches(deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target);
|
|
95
|
+
}
|
|
96
|
+
const follower = deps.runtime.followerRegistry.getByTarget(binding.target);
|
|
97
|
+
if (!follower?.registrationGeneration)
|
|
98
|
+
return undefined;
|
|
99
|
+
const generation = follower.registrationGeneration;
|
|
100
|
+
return () => {
|
|
101
|
+
const current = deps.runtime.followerRegistry.get(follower.instanceId);
|
|
102
|
+
return current?.registrationGeneration === generation && matches(current.target);
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
callApi: deps.callApi,
|
|
106
|
+
}) : undefined;
|
|
107
|
+
const reconcileThreadDisplayOperation = async () => {
|
|
108
|
+
const result = await display.reconcile();
|
|
109
|
+
deps.onThreadDisplayChanged?.();
|
|
110
|
+
return result;
|
|
111
|
+
};
|
|
112
|
+
const reconcileThreadDisplay = () => runWorkspaceOperation({
|
|
113
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
114
|
+
operationKind: "workspace.reconcile-display",
|
|
115
|
+
scopes: [{ kind: "profile" }],
|
|
116
|
+
}, reconcileThreadDisplayOperation);
|
|
117
|
+
const scheduleDisplay = () => {
|
|
118
|
+
if (!display)
|
|
119
|
+
return;
|
|
120
|
+
void reconcileThreadDisplay().catch((error) => {
|
|
121
|
+
deps.recordRuntimeEvent("bus", error, { phase: "thread-display-reconcile" });
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
let modeTail = Promise.resolve();
|
|
125
|
+
const applyThreadDisplayMode = (mode, isCurrent) => {
|
|
126
|
+
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
127
|
+
const profile = deps.getTelegramProfile?.();
|
|
128
|
+
const current = () => epoch !== undefined && deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
129
|
+
deps.getTelegramProfile?.() === profile && isCurrent();
|
|
130
|
+
const run = modeTail.then(() => runWorkspaceOperation({
|
|
131
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
132
|
+
operationKind: "workspace.set-display-mode",
|
|
133
|
+
scopes: [{ kind: "profile" }],
|
|
134
|
+
}, async () => {
|
|
135
|
+
if (!display || !deps.persistThreadDisplayMode || !current()) {
|
|
136
|
+
throw new Error("Telegram Thread display setting requires current leader authority.");
|
|
137
|
+
}
|
|
138
|
+
const assertDisplayPeers = () => {
|
|
139
|
+
const requiredCapability = mode === "directory-snake" || mode === "directory-title"
|
|
140
|
+
? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
|
|
141
|
+
: TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
|
|
142
|
+
if (mode !== "names" && deps.runtime.followerRegistry.list().some((follower) => !hasTelegramBusCapability(follower.protocol, requiredCapability)))
|
|
143
|
+
throw new Error("Update or restart all connected followers before changing Thread display mode.");
|
|
144
|
+
};
|
|
145
|
+
assertDisplayPeers();
|
|
146
|
+
await deps.persistThreadDisplayMode(mode, current);
|
|
147
|
+
assertDisplayPeers();
|
|
148
|
+
if (!current() || deps.getThreadDisplayMode?.() !== mode) {
|
|
149
|
+
throw new Error("Telegram Thread display preference changed before application.");
|
|
150
|
+
}
|
|
151
|
+
await reconcileThreadDisplayOperation();
|
|
152
|
+
if (!current() || deps.getThreadDisplayMode?.() !== mode) {
|
|
153
|
+
throw new Error("Telegram Thread display application lost its originating authority.");
|
|
154
|
+
}
|
|
155
|
+
}));
|
|
156
|
+
modeTail = run.catch(() => undefined);
|
|
157
|
+
return run;
|
|
158
|
+
};
|
|
159
|
+
const provisionLeaderTarget = createTelegramBusLeaderTargetProvisioner({
|
|
160
|
+
...provisionerPorts,
|
|
161
|
+
instanceId: deps.instanceId,
|
|
162
|
+
getCwd: deps.getCwd,
|
|
163
|
+
getSessionId: deps.getSessionId,
|
|
164
|
+
getTelegramProfile: deps.getTelegramProfile,
|
|
165
|
+
shouldForceFreshUnnamed: deps.shouldForceFreshUnnamed,
|
|
166
|
+
getRequestedThreadName: deps.getRequestedThreadName,
|
|
167
|
+
getThreadReconciliationMachineState: deps.getThreadReconciliationMachineState,
|
|
168
|
+
recordThreadReconciliationPlan: deps.recordThreadReconciliationPlan,
|
|
169
|
+
setLeaderTarget: deps.setLeaderTarget,
|
|
170
|
+
});
|
|
171
|
+
const disconnectFollower = createTelegramBusFollowerDisconnectHandler({
|
|
172
|
+
...provisionerPorts,
|
|
173
|
+
});
|
|
174
|
+
const cleanupConfirmedDeadFollower = createTelegramBusFollowerConfirmedDeadHandler({
|
|
175
|
+
...provisionerPorts,
|
|
176
|
+
});
|
|
177
|
+
const provisionFollowerTarget = createTelegramBusFollowerTargetProvisioner({
|
|
178
|
+
...provisionerPorts,
|
|
179
|
+
});
|
|
180
|
+
const renameLeaderThreadAdmitted = async (threadName, expectedTarget) => {
|
|
181
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
182
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
183
|
+
throw new Error("Telegram Workspace Thread rename requires leader ownership.");
|
|
184
|
+
}
|
|
185
|
+
const record = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
186
|
+
records: deps.topicTargetStore.list(),
|
|
187
|
+
instanceId: deps.instanceId,
|
|
188
|
+
});
|
|
189
|
+
if (typeof record?.target.threadId !== "number") {
|
|
190
|
+
throw new Error("No Workspace Thread is bound to this Pi instance.");
|
|
191
|
+
}
|
|
192
|
+
if (expectedTarget && (record.target.chatId !== expectedTarget.chatId ||
|
|
193
|
+
record.target.threadId !== expectedTarget.threadId)) {
|
|
194
|
+
throw new Error("Telegram Workspace Thread rename target changed.");
|
|
195
|
+
}
|
|
196
|
+
const rename = Threads.createTelegramTopicTargetRenamer({
|
|
197
|
+
store: deps.topicTargetStore,
|
|
198
|
+
callApi: deps.callApi,
|
|
199
|
+
assertAuthority() {
|
|
200
|
+
if (deps.getCurrentLeaderEpoch &&
|
|
201
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch) {
|
|
202
|
+
throw new Error("Telegram Workspace Thread rename lost leader ownership.");
|
|
203
|
+
}
|
|
204
|
+
const current = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
205
|
+
records: deps.topicTargetStore.list(),
|
|
206
|
+
instanceId: deps.instanceId,
|
|
207
|
+
});
|
|
208
|
+
if (current?.target.chatId !== record.target.chatId ||
|
|
209
|
+
current.target.threadId !== record.target.threadId) {
|
|
210
|
+
throw new Error("Telegram Workspace Thread rename target changed.");
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
const renamed = await rename({
|
|
215
|
+
target: {
|
|
216
|
+
chatId: record.target.chatId,
|
|
217
|
+
threadId: record.target.threadId,
|
|
218
|
+
},
|
|
219
|
+
threadName,
|
|
220
|
+
slot: record.slot,
|
|
221
|
+
});
|
|
222
|
+
if (!renamed) {
|
|
223
|
+
throw new Error(Threads.getTelegramTopicThreadNameValidationError(threadName, record.slot) ?? "Telegram Workspace Thread name is already reserved.");
|
|
224
|
+
}
|
|
225
|
+
await deps.topicTargetStore.persist();
|
|
226
|
+
return renamed;
|
|
227
|
+
};
|
|
228
|
+
const resetThreadNameAdmitted = async (target, assertTargetCurrent = () => undefined) => {
|
|
229
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
230
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
231
|
+
throw new Error("Telegram Workspace Thread reset requires leader ownership.");
|
|
232
|
+
}
|
|
233
|
+
const binding = deps.topicTargetStore.listWorkspaceBindings().find((candidate) => candidate.target.chatId === target.chatId &&
|
|
234
|
+
candidate.target.threadId === target.threadId);
|
|
235
|
+
if (!binding)
|
|
236
|
+
throw new Error("No Workspace Thread binding is available to reset.");
|
|
237
|
+
const automaticTitle = resolveTelegramInitialWorkspaceDisplayName({
|
|
238
|
+
bindings: deps.topicTargetStore.listWorkspaceBindings(),
|
|
239
|
+
binding: { ...binding, manualThreadName: undefined },
|
|
240
|
+
mode: deps.getThreadDisplayMode?.() ?? "letters",
|
|
241
|
+
preserveRetainedManualName: false,
|
|
242
|
+
});
|
|
243
|
+
if (!automaticTitle) {
|
|
244
|
+
throw new Error("Telegram Workspace automatic title is unavailable.");
|
|
245
|
+
}
|
|
246
|
+
assertTargetCurrent();
|
|
247
|
+
await deps.callApi("editForumTopic", {
|
|
248
|
+
chat_id: target.chatId,
|
|
249
|
+
message_thread_id: target.threadId,
|
|
250
|
+
name: automaticTitle,
|
|
251
|
+
});
|
|
252
|
+
if (deps.getCurrentLeaderEpoch &&
|
|
253
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch) {
|
|
254
|
+
throw new Error("Telegram Workspace Thread reset lost leader ownership.");
|
|
255
|
+
}
|
|
256
|
+
assertTargetCurrent();
|
|
257
|
+
const reset = deps.topicTargetStore.clearManualNameByTarget(target, automaticTitle);
|
|
258
|
+
if (!reset)
|
|
259
|
+
throw new Error("Telegram Workspace Thread reset changed binding.");
|
|
260
|
+
await deps.topicTargetStore.persist();
|
|
261
|
+
return { threadName: automaticTitle };
|
|
262
|
+
};
|
|
263
|
+
const renameLeaderThread = (threadName) => runWorkspaceOperation({
|
|
264
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
265
|
+
operationKind: "workspace.rename-leader",
|
|
266
|
+
scopes: [{ kind: "profile" }],
|
|
267
|
+
}, () => renameLeaderThreadAdmitted(threadName));
|
|
268
|
+
const resetLeaderThreadName = (expectedTarget) => runWorkspaceOperation({
|
|
269
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
270
|
+
operationKind: "workspace.reset-leader-name",
|
|
271
|
+
scopes: [{ kind: "profile" }],
|
|
272
|
+
}, () => {
|
|
273
|
+
const record = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
274
|
+
records: deps.topicTargetStore.list(),
|
|
275
|
+
instanceId: deps.instanceId,
|
|
276
|
+
});
|
|
277
|
+
if (!record || typeof record.target.threadId !== "number") {
|
|
278
|
+
throw new Error("No Workspace Thread is bound to this Pi instance.");
|
|
279
|
+
}
|
|
280
|
+
if (record.target.chatId !== expectedTarget.chatId ||
|
|
281
|
+
record.target.threadId !== expectedTarget.threadId) {
|
|
282
|
+
throw new Error("Telegram Workspace Thread reset target changed.");
|
|
283
|
+
}
|
|
284
|
+
return resetThreadNameAdmitted(record.target, () => {
|
|
285
|
+
const current = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
286
|
+
records: deps.topicTargetStore.list(),
|
|
287
|
+
instanceId: deps.instanceId,
|
|
288
|
+
});
|
|
289
|
+
if (current?.target.chatId !== expectedTarget.chatId ||
|
|
290
|
+
current.target.threadId !== expectedTarget.threadId) {
|
|
291
|
+
throw new Error("Telegram Workspace Thread reset target changed.");
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
const runtime = createTelegramBusLeaderRuntime({
|
|
296
|
+
...deps.runtime,
|
|
297
|
+
applyThreadDisplayMode,
|
|
298
|
+
getThreadDisplayMode: deps.getThreadDisplayMode,
|
|
299
|
+
onFollowerRegistered: scheduleDisplay,
|
|
300
|
+
provisionLeaderTarget: (ctx) => {
|
|
301
|
+
const chatId = deps.getAllowedUserId();
|
|
302
|
+
return runWorkspaceOperation({
|
|
303
|
+
operationId: `leader-provision:${deps.instanceId}`,
|
|
304
|
+
operationKind: "workspace.provision-leader",
|
|
305
|
+
scopes: [
|
|
306
|
+
typeof chatId === "number"
|
|
307
|
+
? { kind: "chat", chatId }
|
|
308
|
+
: { kind: "profile" },
|
|
309
|
+
],
|
|
310
|
+
}, () => provisionLeaderTarget(ctx));
|
|
311
|
+
},
|
|
312
|
+
getFollowerDisplayTitle(follower) {
|
|
313
|
+
const binding = deps.topicTargetStore.listWorkspaceBindings().find((binding) => binding.target.chatId === follower.target?.chatId &&
|
|
314
|
+
binding.target.threadId === follower.target?.threadId);
|
|
315
|
+
return binding?.displayTitle ?? binding?.threadName;
|
|
316
|
+
},
|
|
317
|
+
onFollowerDisconnected: async (follower) => {
|
|
318
|
+
await runWorkspaceOperation({
|
|
319
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
320
|
+
operationKind: "workspace.disconnect-follower",
|
|
321
|
+
scopes: [{ kind: "profile" }],
|
|
322
|
+
}, () => disconnectFollower(follower));
|
|
323
|
+
scheduleDisplay();
|
|
324
|
+
},
|
|
325
|
+
async renameFollowerThread(follower, threadName) {
|
|
326
|
+
return runWorkspaceOperation({
|
|
327
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
328
|
+
operationKind: "workspace.rename-follower",
|
|
329
|
+
scopes: [{ kind: "profile" }],
|
|
330
|
+
}, async () => {
|
|
331
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
332
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
333
|
+
throw new Error("Telegram Workspace Thread rename requires leader ownership.");
|
|
334
|
+
}
|
|
335
|
+
if (typeof follower.target?.threadId !== "number") {
|
|
336
|
+
throw new Error("Telegram follower has no bound Workspace Thread.");
|
|
337
|
+
}
|
|
338
|
+
const rename = Threads.createTelegramTopicTargetRenamer({
|
|
339
|
+
store: deps.topicTargetStore,
|
|
340
|
+
callApi: deps.callApi,
|
|
341
|
+
assertAuthority() {
|
|
342
|
+
if (deps.getCurrentLeaderEpoch &&
|
|
343
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch) {
|
|
344
|
+
throw new Error("Telegram Workspace Thread rename lost leader ownership.");
|
|
345
|
+
}
|
|
346
|
+
const current = deps.runtime.followerRegistry.get(follower.instanceId);
|
|
347
|
+
if (current?.registrationGeneration !== follower.registrationGeneration ||
|
|
348
|
+
current?.target?.chatId !== follower.target?.chatId ||
|
|
349
|
+
current?.target?.threadId !== follower.target?.threadId) {
|
|
350
|
+
throw new Error("Telegram follower Workspace Thread target changed.");
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
const renamed = await rename({
|
|
355
|
+
target: {
|
|
356
|
+
chatId: follower.target.chatId,
|
|
357
|
+
threadId: follower.target.threadId,
|
|
358
|
+
},
|
|
359
|
+
threadName,
|
|
360
|
+
slot: follower.slot,
|
|
361
|
+
});
|
|
362
|
+
if (deps.getCurrentLeaderEpoch &&
|
|
363
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch) {
|
|
364
|
+
throw new Error("Telegram Workspace Thread rename lost leader ownership.");
|
|
365
|
+
}
|
|
366
|
+
if (!renamed) {
|
|
367
|
+
throw new Error(Threads.getTelegramTopicThreadNameValidationError(threadName, follower.slot) ?? "Telegram Workspace Thread name is already reserved.");
|
|
368
|
+
}
|
|
369
|
+
await deps.topicTargetStore.persist();
|
|
370
|
+
return { threadName: renamed.manualThreadName ?? threadName };
|
|
371
|
+
});
|
|
372
|
+
},
|
|
373
|
+
resetFollowerThreadName: (follower) => runWorkspaceOperation({
|
|
374
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
375
|
+
operationKind: "workspace.reset-follower-name",
|
|
376
|
+
scopes: [{ kind: "profile" }],
|
|
377
|
+
}, () => {
|
|
378
|
+
if (!follower.target || typeof follower.target.threadId !== "number") {
|
|
379
|
+
throw new Error("Telegram follower has no bound Workspace Thread.");
|
|
380
|
+
}
|
|
381
|
+
const target = {
|
|
382
|
+
chatId: follower.target.chatId,
|
|
383
|
+
threadId: follower.target.threadId,
|
|
384
|
+
};
|
|
385
|
+
return resetThreadNameAdmitted(target, () => {
|
|
386
|
+
const current = deps.runtime.followerRegistry.get(follower.instanceId);
|
|
387
|
+
if (current?.registrationGeneration !== follower.registrationGeneration ||
|
|
388
|
+
current?.target?.chatId !== target.chatId ||
|
|
389
|
+
current?.target?.threadId !== target.threadId) {
|
|
390
|
+
throw new Error("Telegram follower Workspace Thread target changed.");
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}),
|
|
394
|
+
onFollowerConfirmedDead: async (follower) => {
|
|
395
|
+
await runWorkspaceOperation({
|
|
396
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
397
|
+
operationKind: "workspace.cleanup-dead-follower",
|
|
398
|
+
scopes: [{ kind: "profile" }],
|
|
399
|
+
}, () => cleanupConfirmedDeadFollower(follower));
|
|
400
|
+
scheduleDisplay();
|
|
401
|
+
},
|
|
402
|
+
provisionFollowerTarget: (registration, options) => runWorkspaceOperation({
|
|
403
|
+
operationId: `follower-provision:${registration.instanceId}:${registration.registrationGeneration}`,
|
|
404
|
+
operationKind: "workspace.provision-follower",
|
|
405
|
+
scopes: [{ kind: "profile" }],
|
|
406
|
+
}, () => provisionFollowerTarget(registration, options)),
|
|
407
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
408
|
+
runWorkspaceAdmission,
|
|
409
|
+
callApi: createTelegramBusLeaderApiProxy({
|
|
410
|
+
call: deps.callApi,
|
|
411
|
+
callMultipart: deps.callMultipart,
|
|
412
|
+
downloadFile: deps.downloadFile,
|
|
413
|
+
recoverStaleTargetError: deps.recoverStaleTargetError,
|
|
414
|
+
}),
|
|
415
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
416
|
+
});
|
|
417
|
+
const assembled = {
|
|
418
|
+
...runtime,
|
|
419
|
+
runWorkspaceOperation,
|
|
420
|
+
renameLeaderThread,
|
|
421
|
+
renameLeaderThreadAdmitted,
|
|
422
|
+
resetLeaderThreadName,
|
|
423
|
+
resetThreadNameAdmitted,
|
|
424
|
+
captureWorkspaceExternalProtection: deps.captureWorkspaceExternalProtection,
|
|
425
|
+
async startPolling(ctx) {
|
|
426
|
+
await runtime.startPolling(ctx);
|
|
427
|
+
if (display)
|
|
428
|
+
scheduleDisplay();
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
if (!display)
|
|
432
|
+
return assembled;
|
|
433
|
+
return {
|
|
434
|
+
...assembled,
|
|
435
|
+
reconcileThreadDisplay,
|
|
436
|
+
setThreadDisplayMode: (mode) => applyThreadDisplayMode(mode, () => true),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
export function createTelegramBusInstanceLifecycleAnnouncement(input) {
|
|
440
|
+
return {
|
|
441
|
+
target: { ...input.target },
|
|
442
|
+
text: `<b>📡 Instance <i>${formatTelegramBusInstanceLabel(input)}</i> ${input.state}.</b>`,
|
|
443
|
+
parseMode: "HTML",
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const TELEGRAM_BUS_SLOW_FOLLOWER_REGISTRATION_MS = 1000;
|
|
447
|
+
function scheduleTelegramBusLeaderBackgroundTask(task, onError) {
|
|
448
|
+
const timer = setTimeout(() => {
|
|
449
|
+
void task().catch((error) => {
|
|
450
|
+
try {
|
|
451
|
+
onError(error);
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// Background diagnostics cannot create an unhandled timer rejection.
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}, 0);
|
|
458
|
+
timer.unref?.();
|
|
459
|
+
}
|
|
460
|
+
function recordSlowTelegramBusFollowerRegistrationStep(deps, input) {
|
|
461
|
+
if (input.elapsedMs < TELEGRAM_BUS_SLOW_FOLLOWER_REGISTRATION_MS)
|
|
462
|
+
return;
|
|
463
|
+
deps.recordRuntimeEvent("bus", `Telegram bus follower registration step was slow (${input.elapsedMs}ms).`, {
|
|
464
|
+
phase: input.phase,
|
|
465
|
+
elapsedMs: input.elapsedMs,
|
|
466
|
+
instanceId: input.instanceId,
|
|
467
|
+
reused: input.reused,
|
|
468
|
+
chatId: input.target?.chatId,
|
|
469
|
+
threadId: input.target?.threadId,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
export function createTelegramBusFollowerTargetProvisioner(deps) {
|
|
473
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
474
|
+
const pendingRegistrations = new Map();
|
|
475
|
+
return async (registration, options) => {
|
|
476
|
+
if (options?.existingWorkspaceBindingOnly && !registration.cwd) {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
const registrationStartedAtMs = Date.now();
|
|
480
|
+
const chatId = deps.getAllowedUserId();
|
|
481
|
+
if (typeof chatId !== "number")
|
|
482
|
+
return registration.target;
|
|
483
|
+
await deps.topicTargetStore.load();
|
|
484
|
+
let capacityUnavailable = false;
|
|
485
|
+
const workspaceIdentity = registration.cwd
|
|
486
|
+
? deps.topicTargetStore.claimWorkspaceIdentity(registration.cwd, registration.instanceId, registration.previousInstanceId, {
|
|
487
|
+
existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
|
|
488
|
+
sessionId: registration.sessionId,
|
|
489
|
+
onCapacityUnavailable() {
|
|
490
|
+
capacityUnavailable = true;
|
|
491
|
+
},
|
|
492
|
+
})
|
|
493
|
+
: undefined;
|
|
494
|
+
if (registration.cwd && !workspaceIdentity) {
|
|
495
|
+
if (options?.existingWorkspaceBindingOnly)
|
|
496
|
+
return undefined;
|
|
497
|
+
if (capacityUnavailable) {
|
|
498
|
+
throw new Error("Telegram Workspace slot reservation is unavailable.");
|
|
499
|
+
}
|
|
500
|
+
throw new Error("Telegram Workspace identity is already claimed.");
|
|
501
|
+
}
|
|
502
|
+
const workspaceBinding = workspaceIdentity
|
|
503
|
+
? deps.topicTargetStore.getWorkspaceBinding(workspaceIdentity.cwd, workspaceIdentity.instanceSlot, workspaceIdentity.sessionId)
|
|
504
|
+
: undefined;
|
|
505
|
+
const provision = Threads.createTelegramTopicTargetProvisioner({
|
|
506
|
+
topicChatId: chatId,
|
|
507
|
+
store: deps.topicTargetStore,
|
|
508
|
+
getNowMs,
|
|
509
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
510
|
+
callApi: deps.callApi,
|
|
511
|
+
// Manual follower registration should create a visible fresh topic unless
|
|
512
|
+
// the same profile key already has a known live/reusable binding. Do not
|
|
513
|
+
// silently claim old offline/failed tabs: they may be closed/deleted in
|
|
514
|
+
// Telegram and therefore invisible to the operator.
|
|
515
|
+
claimPendingTargets: false,
|
|
516
|
+
resolveInitialWorkspaceDisplayTitle: deps.resolveInitialWorkspaceDisplayTitle,
|
|
517
|
+
});
|
|
518
|
+
const recordsBeforeProvision = deps.topicTargetStore.list();
|
|
519
|
+
const followerProfileKey = registration.profileKey ?? `manual:${registration.instanceId}`;
|
|
520
|
+
const requestedTarget = registration.target ?? workspaceBinding?.target;
|
|
521
|
+
const reconnectRecord = recordsBeforeProvision.find((record) => {
|
|
522
|
+
const matchesRequestedTarget = !requestedTarget ||
|
|
523
|
+
(record.target.chatId === requestedTarget.chatId &&
|
|
524
|
+
record.target.threadId === requestedTarget.threadId);
|
|
525
|
+
const matchesCurrentIdentity = record.instanceId === registration.instanceId ||
|
|
526
|
+
record.profileKey === followerProfileKey;
|
|
527
|
+
const matchesSessionHandoff = !!requestedTarget &&
|
|
528
|
+
!!registration.previousInstanceId &&
|
|
529
|
+
record.instanceId === registration.previousInstanceId &&
|
|
530
|
+
matchesRequestedTarget;
|
|
531
|
+
const matchesWorkspaceRecovery = !!workspaceBinding &&
|
|
532
|
+
record.status === "probe-required" &&
|
|
533
|
+
matchesRequestedTarget;
|
|
534
|
+
return (record.owner?.kind === "manual-follower" &&
|
|
535
|
+
((matchesCurrentIdentity && matchesRequestedTarget) ||
|
|
536
|
+
matchesSessionHandoff || matchesWorkspaceRecovery));
|
|
537
|
+
});
|
|
538
|
+
const followerOwner = Threads.getTelegramThreadOwnerFromProfileKey(followerProfileKey);
|
|
539
|
+
const recoverableTarget = !reconnectRecord &&
|
|
540
|
+
requestedTarget?.chatId === chatId &&
|
|
541
|
+
requestedTarget.threadId !== undefined &&
|
|
542
|
+
!recordsBeforeProvision.some((record) => record.target.chatId === requestedTarget.chatId &&
|
|
543
|
+
record.target.threadId === requestedTarget.threadId)
|
|
544
|
+
? requestedTarget
|
|
545
|
+
: undefined;
|
|
546
|
+
const recoveryHint = recoverableTarget
|
|
547
|
+
? deps.topicTargetStore.getFollowerRecoveryHintByTarget?.(recoverableTarget)
|
|
548
|
+
: undefined;
|
|
549
|
+
const registrationKey = workspaceIdentity?.bindingKey ||
|
|
550
|
+
followerProfileKey ||
|
|
551
|
+
registration.instanceId;
|
|
552
|
+
const pendingRegistration = pendingRegistrations.get(registrationKey);
|
|
553
|
+
if (pendingRegistration)
|
|
554
|
+
return pendingRegistration;
|
|
555
|
+
const provisionTarget = async () => {
|
|
556
|
+
deps.onProvisioningStart?.();
|
|
557
|
+
try {
|
|
558
|
+
return await provision({
|
|
559
|
+
instanceId: registration.instanceId,
|
|
560
|
+
owner: followerOwner.kind === "manual-follower"
|
|
561
|
+
? followerOwner
|
|
562
|
+
: {
|
|
563
|
+
kind: "manual-follower",
|
|
564
|
+
instanceId: registration.instanceId,
|
|
565
|
+
},
|
|
566
|
+
profileKey: followerProfileKey,
|
|
567
|
+
threadName: workspaceBinding?.threadName ?? registration.threadName,
|
|
568
|
+
preferredSlot: workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot,
|
|
569
|
+
...(workspaceIdentity
|
|
570
|
+
? {
|
|
571
|
+
workspaceBindingKey: workspaceIdentity.bindingKey,
|
|
572
|
+
workspaceCwd: workspaceIdentity.cwd,
|
|
573
|
+
}
|
|
574
|
+
: {}),
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
finally {
|
|
578
|
+
deps.onProvisioningEnd?.();
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
const recoverRequestedTarget = async () => {
|
|
582
|
+
const nowMs = getNowMs();
|
|
583
|
+
const carriedThreadName = workspaceBinding?.threadName ?? registration.threadName;
|
|
584
|
+
const requestedThreadName = carriedThreadName &&
|
|
585
|
+
Threads.isTelegramTopicThreadNameValidForSlot(carriedThreadName, registration.slot)
|
|
586
|
+
? carriedThreadName
|
|
587
|
+
: recoveryHint?.threadName &&
|
|
588
|
+
Threads.isTelegramTopicThreadNameValidForSlot(recoveryHint.threadName, recoveryHint.slot)
|
|
589
|
+
? recoveryHint.threadName
|
|
590
|
+
: undefined;
|
|
591
|
+
const requestedSlot = workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot ?? recoveryHint?.slot;
|
|
592
|
+
const recoveredSlot = deps.topicTargetStore.allocateSlot(followerProfileKey, requestedSlot, workspaceIdentity?.bindingKey);
|
|
593
|
+
if (!recoveredSlot) {
|
|
594
|
+
throw new Error("Telegram Workspace slot reservation is unavailable.");
|
|
595
|
+
}
|
|
596
|
+
const recoveredRecord = {
|
|
597
|
+
profileKey: followerProfileKey,
|
|
598
|
+
owner: followerOwner.kind === "manual-follower"
|
|
599
|
+
? followerOwner
|
|
600
|
+
: {
|
|
601
|
+
kind: "manual-follower",
|
|
602
|
+
instanceId: registration.instanceId,
|
|
603
|
+
},
|
|
604
|
+
target: {
|
|
605
|
+
chatId: recoverableTarget.chatId,
|
|
606
|
+
threadId: recoverableTarget.threadId,
|
|
607
|
+
},
|
|
608
|
+
status: "active",
|
|
609
|
+
createdAtMs: registration.connectedAtMs || nowMs,
|
|
610
|
+
updatedAtMs: nowMs,
|
|
611
|
+
instanceId: registration.instanceId,
|
|
612
|
+
...(requestedThreadName ? { threadName: requestedThreadName } : {}),
|
|
613
|
+
slot: recoveredSlot,
|
|
614
|
+
lastSyncObservedAtMs: nowMs,
|
|
615
|
+
lastReconcileAction: "follower-live-target-recovery",
|
|
616
|
+
};
|
|
617
|
+
return {
|
|
618
|
+
target: recoveredRecord.target,
|
|
619
|
+
reused: true,
|
|
620
|
+
record: recoveredRecord,
|
|
621
|
+
};
|
|
622
|
+
};
|
|
623
|
+
const runRegistration = async () => {
|
|
624
|
+
const recoveryTarget = reconnectRecord?.target ?? recoverableTarget;
|
|
625
|
+
if (recoveryTarget) {
|
|
626
|
+
Threads.assertTelegramPendingTopicRecoveryAllowed(deps.topicTargetStore, recoveryTarget);
|
|
627
|
+
}
|
|
628
|
+
const pendingTargetRecovery = recoverableTarget && deps.topicTargetStore.listPendingProvisions().some((entry) => entry.target?.chatId === recoverableTarget.chatId &&
|
|
629
|
+
entry.target?.threadId === recoverableTarget.threadId &&
|
|
630
|
+
(entry.instanceId === registration.instanceId || entry.profileKey === followerProfileKey));
|
|
631
|
+
let result = reconnectRecord
|
|
632
|
+
? {
|
|
633
|
+
target: reconnectRecord.target,
|
|
634
|
+
reused: true,
|
|
635
|
+
record: reconnectRecord,
|
|
636
|
+
}
|
|
637
|
+
: recoverableTarget && !pendingTargetRecovery
|
|
638
|
+
? await recoverRequestedTarget()
|
|
639
|
+
: await provisionTarget();
|
|
640
|
+
const crossSessionReuse = !!reconnectRecord &&
|
|
641
|
+
reconnectRecord.instanceId !== registration.instanceId;
|
|
642
|
+
if (reconnectRecord && !crossSessionReuse) {
|
|
643
|
+
const nowMs = getNowMs();
|
|
644
|
+
const refreshedRecord = deps.topicTargetStore.upsert({
|
|
645
|
+
...reconnectRecord,
|
|
646
|
+
instanceId: registration.instanceId,
|
|
647
|
+
updatedAtMs: nowMs,
|
|
648
|
+
lastSyncObservedAtMs: nowMs,
|
|
649
|
+
lastReconcileAction: "follower-register-reuse",
|
|
650
|
+
});
|
|
651
|
+
await deps.topicTargetStore.persist();
|
|
652
|
+
result = {
|
|
653
|
+
target: refreshedRecord.target,
|
|
654
|
+
reused: true,
|
|
655
|
+
record: refreshedRecord,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
const probeRequiredRecord = reconnectRecord?.status === "probe-required";
|
|
659
|
+
const exactSessionHandoff = crossSessionReuse &&
|
|
660
|
+
!!requestedTarget &&
|
|
661
|
+
registration.previousInstanceId === reconnectRecord?.instanceId &&
|
|
662
|
+
requestedTarget.chatId === reconnectRecord.target.chatId &&
|
|
663
|
+
requestedTarget.threadId === reconnectRecord.target.threadId;
|
|
664
|
+
const requiresVisibilityProbe = crossSessionReuse ||
|
|
665
|
+
probeRequiredRecord ||
|
|
666
|
+
recoverableTarget !== undefined;
|
|
667
|
+
let connectedAnnouncement = !result.reused || requiresVisibilityProbe
|
|
668
|
+
? createTelegramBusInstanceLifecycleAnnouncement({
|
|
669
|
+
target: result.target,
|
|
670
|
+
threadName: result.displayTitle ??
|
|
671
|
+
(workspaceBinding?.target.chatId === result.target.chatId &&
|
|
672
|
+
workspaceBinding.target.threadId === result.target.threadId
|
|
673
|
+
? workspaceBinding.displayTitle : undefined) ?? result.record.threadName,
|
|
674
|
+
slot: result.record.slot,
|
|
675
|
+
state: "connected",
|
|
676
|
+
})
|
|
677
|
+
: undefined;
|
|
678
|
+
if (requiresVisibilityProbe && connectedAnnouncement) {
|
|
679
|
+
try {
|
|
680
|
+
await deps.callApi(exactSessionHandoff ? "sendChatAction" : "sendMessage", exactSessionHandoff
|
|
681
|
+
? {
|
|
682
|
+
chat_id: connectedAnnouncement.target.chatId,
|
|
683
|
+
message_thread_id: connectedAnnouncement.target.threadId,
|
|
684
|
+
action: "typing",
|
|
685
|
+
}
|
|
686
|
+
: {
|
|
687
|
+
chat_id: connectedAnnouncement.target.chatId,
|
|
688
|
+
message_thread_id: connectedAnnouncement.target.threadId,
|
|
689
|
+
text: connectedAnnouncement.text,
|
|
690
|
+
parse_mode: connectedAnnouncement.parseMode,
|
|
691
|
+
});
|
|
692
|
+
if (recoverableTarget || probeRequiredRecord) {
|
|
693
|
+
const activatedRecord = deps.topicTargetStore.upsert({
|
|
694
|
+
...result.record,
|
|
695
|
+
...(probeRequiredRecord && crossSessionReuse
|
|
696
|
+
? {
|
|
697
|
+
profileKey: followerProfileKey,
|
|
698
|
+
owner: followerOwner.kind === "manual-follower"
|
|
699
|
+
? followerOwner
|
|
700
|
+
: {
|
|
701
|
+
kind: "manual-follower",
|
|
702
|
+
instanceId: registration.instanceId,
|
|
703
|
+
},
|
|
704
|
+
instanceId: registration.instanceId,
|
|
705
|
+
}
|
|
706
|
+
: {}),
|
|
707
|
+
status: "active",
|
|
708
|
+
updatedAtMs: getNowMs(),
|
|
709
|
+
lastSyncObservedAtMs: getNowMs(),
|
|
710
|
+
lastReconcileAction: "follower-live-target-recovery",
|
|
711
|
+
});
|
|
712
|
+
await deps.topicTargetStore.persist();
|
|
713
|
+
result = {
|
|
714
|
+
target: activatedRecord.target,
|
|
715
|
+
reused: true,
|
|
716
|
+
record: activatedRecord,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
else if (crossSessionReuse && reconnectRecord) {
|
|
720
|
+
const nowMs = getNowMs();
|
|
721
|
+
const transferredRecord = deps.topicTargetStore.upsert({
|
|
722
|
+
...reconnectRecord,
|
|
723
|
+
profileKey: followerProfileKey,
|
|
724
|
+
owner: followerOwner.kind === "manual-follower"
|
|
725
|
+
? followerOwner
|
|
726
|
+
: {
|
|
727
|
+
kind: "manual-follower",
|
|
728
|
+
instanceId: registration.instanceId,
|
|
729
|
+
},
|
|
730
|
+
instanceId: registration.instanceId,
|
|
731
|
+
updatedAtMs: nowMs,
|
|
732
|
+
lastSyncObservedAtMs: nowMs,
|
|
733
|
+
lastReconcileAction: "follower-session-handoff",
|
|
734
|
+
});
|
|
735
|
+
await deps.topicTargetStore.persist();
|
|
736
|
+
result = {
|
|
737
|
+
target: transferredRecord.target,
|
|
738
|
+
reused: true,
|
|
739
|
+
record: transferredRecord,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
connectedAnnouncement = undefined;
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
if (Threads.isTelegramTopicTargetStaleError(error)) {
|
|
746
|
+
deps.topicTargetStore.markStaleByTarget(result.target, "deleted", error instanceof Error ? error.message : String(error));
|
|
747
|
+
await deps.topicTargetStore.persist();
|
|
748
|
+
result = await provisionTarget();
|
|
749
|
+
connectedAnnouncement =
|
|
750
|
+
createTelegramBusInstanceLifecycleAnnouncement({
|
|
751
|
+
target: result.target,
|
|
752
|
+
threadName: result.displayTitle ?? result.record.threadName,
|
|
753
|
+
slot: result.record.slot,
|
|
754
|
+
state: "connected",
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
deps.recordRuntimeEvent("telegram", error, {
|
|
759
|
+
phase: "follower-topic-reuse-probe",
|
|
760
|
+
instanceId: registration.instanceId,
|
|
761
|
+
chatId: result.target.chatId,
|
|
762
|
+
threadId: result.target.threadId,
|
|
763
|
+
});
|
|
764
|
+
if (recoverableTarget) {
|
|
765
|
+
deps.topicTargetStore.upsert({
|
|
766
|
+
...result.record,
|
|
767
|
+
status: "probe-required",
|
|
768
|
+
updatedAtMs: getNowMs(),
|
|
769
|
+
lastSyncError: error instanceof Error ? error.message : String(error),
|
|
770
|
+
lastReconcileAction: "follower-visibility-probe-required",
|
|
771
|
+
});
|
|
772
|
+
await deps.topicTargetStore.persist();
|
|
773
|
+
}
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (workspaceIdentity) {
|
|
779
|
+
const workspaceCommit = Threads.commitTelegramWorkspaceProvisionBinding({
|
|
780
|
+
store: deps.topicTargetStore,
|
|
781
|
+
instanceId: registration.instanceId,
|
|
782
|
+
profileKey: followerProfileKey,
|
|
783
|
+
displayTitle: result.displayTitle,
|
|
784
|
+
binding: {
|
|
785
|
+
...workspaceIdentity,
|
|
786
|
+
target: { ...result.target },
|
|
787
|
+
...(result.record.threadName
|
|
788
|
+
? { threadName: result.record.threadName }
|
|
789
|
+
: {}),
|
|
790
|
+
...(result.record.slot ? { slot: result.record.slot } : {}),
|
|
791
|
+
journalBindingKeys: [followerProfileKey],
|
|
792
|
+
journalBindingsComplete: true,
|
|
793
|
+
updatedAtMs: getNowMs(),
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
if (connectedAnnouncement && workspaceCommit.displayTitle) {
|
|
797
|
+
connectedAnnouncement = createTelegramBusInstanceLifecycleAnnouncement({
|
|
798
|
+
target: result.target, threadName: workspaceCommit.displayTitle,
|
|
799
|
+
slot: result.record.slot, state: "connected",
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
deps.topicTargetStore.markWorkspaceBindingActiveByTarget(result.target);
|
|
803
|
+
await deps.topicTargetStore.persist();
|
|
804
|
+
}
|
|
805
|
+
deps.setSyncState(Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
|
|
806
|
+
nowMs: getNowMs(),
|
|
807
|
+
action: "follower-register",
|
|
808
|
+
}));
|
|
809
|
+
recordSlowTelegramBusFollowerRegistrationStep(deps, {
|
|
810
|
+
phase: "follower-register-critical",
|
|
811
|
+
elapsedMs: Date.now() - registrationStartedAtMs,
|
|
812
|
+
instanceId: registration.instanceId,
|
|
813
|
+
target: result.target,
|
|
814
|
+
reused: result.reused,
|
|
815
|
+
});
|
|
816
|
+
scheduleTelegramBusLeaderBackgroundTask(async () => {
|
|
817
|
+
const backgroundStartedAtMs = Date.now();
|
|
818
|
+
if (connectedAnnouncement) {
|
|
819
|
+
try {
|
|
820
|
+
await deps.callApi("sendMessage", {
|
|
821
|
+
chat_id: connectedAnnouncement.target.chatId,
|
|
822
|
+
message_thread_id: connectedAnnouncement.target.threadId,
|
|
823
|
+
text: connectedAnnouncement.text,
|
|
824
|
+
parse_mode: connectedAnnouncement.parseMode,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
catch (error) {
|
|
828
|
+
deps.recordRuntimeEvent("telegram", error, {
|
|
829
|
+
phase: "follower-topic-announce",
|
|
830
|
+
instanceId: registration.instanceId,
|
|
831
|
+
chatId: result.target.chatId,
|
|
832
|
+
threadId: result.target.threadId,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
const reconcile = async () => {
|
|
837
|
+
await ThreadReconciler.applyThreadReconciliationPlan(ThreadReconciler.planThreadReconciliation({
|
|
838
|
+
nowMs: getNowMs(),
|
|
839
|
+
currentLeaderEpoch: deps.getCurrentLeaderEpoch?.(),
|
|
840
|
+
records: recordsBeforeProvision,
|
|
841
|
+
pendingProvisions: deps.topicTargetStore.listPendingProvisions(),
|
|
842
|
+
replacedBindings: [
|
|
843
|
+
{
|
|
844
|
+
instanceId: registration.instanceId,
|
|
845
|
+
replacementTarget: result.target,
|
|
846
|
+
},
|
|
847
|
+
],
|
|
848
|
+
}), {
|
|
849
|
+
isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore),
|
|
850
|
+
callApi: deps.callApi,
|
|
851
|
+
markStaleByTarget(target, syncStatus, lastSyncError) {
|
|
852
|
+
return deps.topicTargetStore.markStaleByTarget(target, syncStatus, lastSyncError);
|
|
853
|
+
},
|
|
854
|
+
persist() {
|
|
855
|
+
return deps.topicTargetStore.persist();
|
|
856
|
+
},
|
|
857
|
+
removePendingProvisionById(id) {
|
|
858
|
+
return deps.topicTargetStore.removePendingProvision(id);
|
|
859
|
+
},
|
|
860
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
861
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
862
|
+
});
|
|
863
|
+
await deps.topicTargetStore.persist();
|
|
864
|
+
};
|
|
865
|
+
try {
|
|
866
|
+
if (deps.runWorkspaceOperation) {
|
|
867
|
+
await deps.runWorkspaceOperation({
|
|
868
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
869
|
+
operationKind: "workspace.reconcile-follower-provision",
|
|
870
|
+
scopes: [{ kind: "profile" }],
|
|
871
|
+
}, reconcile);
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
await reconcile();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
deps.recordRuntimeEvent("telegram", error, {
|
|
879
|
+
phase: "follower-register-background-reconcile",
|
|
880
|
+
instanceId: registration.instanceId,
|
|
881
|
+
chatId: result.target.chatId,
|
|
882
|
+
threadId: result.target.threadId,
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
recordSlowTelegramBusFollowerRegistrationStep(deps, {
|
|
886
|
+
phase: "follower-register-background",
|
|
887
|
+
elapsedMs: Date.now() - backgroundStartedAtMs,
|
|
888
|
+
instanceId: registration.instanceId,
|
|
889
|
+
target: result.target,
|
|
890
|
+
reused: result.reused,
|
|
891
|
+
});
|
|
892
|
+
}, (error) => {
|
|
893
|
+
deps.recordRuntimeEvent("bus", error, {
|
|
894
|
+
phase: "follower-register-background-owner",
|
|
895
|
+
instanceId: registration.instanceId,
|
|
896
|
+
});
|
|
897
|
+
});
|
|
898
|
+
return {
|
|
899
|
+
...result.target,
|
|
900
|
+
slot: result.record.slot,
|
|
901
|
+
threadName: result.record.threadName,
|
|
902
|
+
};
|
|
903
|
+
};
|
|
904
|
+
const registrationPromise = runRegistration().finally(() => {
|
|
905
|
+
pendingRegistrations.delete(registrationKey);
|
|
906
|
+
deps.topicTargetStore.releaseWorkspaceClaim(registration.instanceId);
|
|
907
|
+
});
|
|
908
|
+
pendingRegistrations.set(registrationKey, registrationPromise);
|
|
909
|
+
return registrationPromise;
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function createTelegramBusFollowerCleanupHandler(deps, trigger) {
|
|
913
|
+
return async (follower) => {
|
|
914
|
+
const target = follower.target;
|
|
915
|
+
if (!target?.threadId)
|
|
916
|
+
return;
|
|
917
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
918
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
919
|
+
throw new Error("Follower disconnect cleanup requires leader ownership.");
|
|
920
|
+
}
|
|
921
|
+
if (!follower.registrationGeneration) {
|
|
922
|
+
throw new Error("Follower disconnect cleanup requires an exact registration generation.");
|
|
923
|
+
}
|
|
924
|
+
const intent = {
|
|
925
|
+
id: `cleanup:${follower.instanceId}:${follower.registrationGeneration}:${target.chatId}:${target.threadId}`,
|
|
926
|
+
owner: "manual-follower",
|
|
927
|
+
instanceId: follower.instanceId,
|
|
928
|
+
runtimeGeneration: follower.registrationGeneration,
|
|
929
|
+
...(follower.profileKey ? { profileKey: follower.profileKey } : {}),
|
|
930
|
+
target: { chatId: target.chatId, threadId: target.threadId },
|
|
931
|
+
requestedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
932
|
+
};
|
|
933
|
+
const departingRecord = deps.topicTargetStore.list().find((record) => record.instanceId === follower.instanceId &&
|
|
934
|
+
record.target.chatId === target.chatId && record.target.threadId === target.threadId);
|
|
935
|
+
const isCleanupTargetProtected = Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore, departingRecord);
|
|
936
|
+
deps.topicTargetStore.upsertPendingCleanup(intent);
|
|
937
|
+
await deps.topicTargetStore.persist();
|
|
938
|
+
const cleanupPlan = ThreadReconciler.planThreadReconciliation({
|
|
939
|
+
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
940
|
+
currentLeaderEpoch: leaderEpoch,
|
|
941
|
+
records: [],
|
|
942
|
+
pendingCleanups: [intent],
|
|
943
|
+
});
|
|
944
|
+
const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(cleanupPlan, {
|
|
945
|
+
isCleanupTargetProtected,
|
|
946
|
+
callApi: deps.callApi,
|
|
947
|
+
markStaleByTarget(target, syncStatus, lastSyncError) {
|
|
948
|
+
const stale = deps.topicTargetStore.markStaleByTarget(target, syncStatus, lastSyncError);
|
|
949
|
+
const inactive = deps.topicTargetStore.markWorkspaceBindingInactiveByTarget(target, (deps.getNowMs ?? Date.now)());
|
|
950
|
+
return stale || inactive;
|
|
951
|
+
},
|
|
952
|
+
removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
|
|
953
|
+
persist: deps.topicTargetStore.persist,
|
|
954
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
955
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
956
|
+
});
|
|
957
|
+
if (cleanupPlan.actions.some((action) => isCleanupTargetProtected(action.target, action)))
|
|
958
|
+
return;
|
|
959
|
+
if (cleanup.incompleteActions?.length) {
|
|
960
|
+
throw new Error("Telegram follower thread deletion was not confirmed; reconnect the leader to retry cleanup.");
|
|
961
|
+
}
|
|
962
|
+
if (deps.getCurrentLeaderEpoch &&
|
|
963
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch) {
|
|
964
|
+
throw new Error("Follower disconnect cleanup lost leader ownership.");
|
|
965
|
+
}
|
|
966
|
+
deps.setSyncState(Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
|
|
967
|
+
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
968
|
+
action: trigger === "confirmed-dead"
|
|
969
|
+
? "manual-follower-confirmed-dead"
|
|
970
|
+
: "manual-follower-disconnect",
|
|
971
|
+
}));
|
|
972
|
+
deps.recordRuntimeEvent("bus", trigger === "confirmed-dead"
|
|
973
|
+
? "Confirmed-dead Telegram bus follower thread cleaned up"
|
|
974
|
+
: "Telegram bus follower disconnected", {
|
|
975
|
+
phase: trigger === "confirmed-dead"
|
|
976
|
+
? "follower-confirmed-dead-cleanup"
|
|
977
|
+
: "follower-disconnect",
|
|
978
|
+
instanceId: follower.instanceId,
|
|
979
|
+
chatId: target.chatId,
|
|
980
|
+
threadId: target.threadId,
|
|
981
|
+
});
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
export function createTelegramBusFollowerDisconnectHandler(deps) {
|
|
985
|
+
return createTelegramBusFollowerCleanupHandler(deps, "graceful-disconnect");
|
|
986
|
+
}
|
|
987
|
+
export function createTelegramBusFollowerConfirmedDeadHandler(deps) {
|
|
988
|
+
return createTelegramBusFollowerCleanupHandler(deps, "confirmed-dead");
|
|
989
|
+
}
|
|
990
|
+
export function createTelegramBusLeaderTargetProvisioner(deps) {
|
|
991
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
992
|
+
return async (ctx) => {
|
|
993
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
994
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
995
|
+
throw new Error("Telegram leader target provisioning requires ownership.");
|
|
996
|
+
}
|
|
997
|
+
await deps.topicTargetStore.load();
|
|
998
|
+
const cwd = deps.getCwd?.(ctx);
|
|
999
|
+
const normalizedCwd = cwd
|
|
1000
|
+
? Threads.normalizeTelegramWorkspacePath(cwd)
|
|
1001
|
+
: undefined;
|
|
1002
|
+
const profileKey = Threads.getTelegramThreadOwnerKey({
|
|
1003
|
+
kind: "leader",
|
|
1004
|
+
cwd: normalizedCwd,
|
|
1005
|
+
instanceId: deps.instanceId,
|
|
1006
|
+
telegramProfile: deps.getTelegramProfile?.(),
|
|
1007
|
+
});
|
|
1008
|
+
const reusableOwnRecord = deps.topicTargetStore.getByProfileKey(profileKey);
|
|
1009
|
+
const pendingCleanups = deps.topicTargetStore.listPendingCleanups();
|
|
1010
|
+
const deferredOwnCleanups = reusableOwnRecord?.status === "active"
|
|
1011
|
+
? pendingCleanups.filter((cleanup) => cleanup.owner === "leader" &&
|
|
1012
|
+
cleanup.target.chatId === reusableOwnRecord.target.chatId &&
|
|
1013
|
+
cleanup.target.threadId === reusableOwnRecord.target.threadId)
|
|
1014
|
+
: [];
|
|
1015
|
+
const deferredOwnCleanupIds = new Set(deferredOwnCleanups.map((cleanup) => cleanup.id));
|
|
1016
|
+
const pendingCleanupPlan = ThreadReconciler.planThreadReconciliation({
|
|
1017
|
+
nowMs: getNowMs(),
|
|
1018
|
+
currentLeaderEpoch: leaderEpoch,
|
|
1019
|
+
previousState: deps.getThreadReconciliationMachineState?.(),
|
|
1020
|
+
records: deps.topicTargetStore.list(),
|
|
1021
|
+
pendingCleanups: pendingCleanups.filter((cleanup) => !deferredOwnCleanupIds.has(cleanup.id)),
|
|
1022
|
+
});
|
|
1023
|
+
deps.recordThreadReconciliationPlan?.(pendingCleanupPlan);
|
|
1024
|
+
const cleanupPorts = {
|
|
1025
|
+
isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore),
|
|
1026
|
+
callApi: deps.callApi,
|
|
1027
|
+
markStaleByTarget: deps.topicTargetStore.markStaleByTarget,
|
|
1028
|
+
removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
|
|
1029
|
+
persist: deps.topicTargetStore.persist,
|
|
1030
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
1031
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1032
|
+
};
|
|
1033
|
+
await ThreadReconciler.applyThreadReconciliationPlan(pendingCleanupPlan, cleanupPorts);
|
|
1034
|
+
deps.onProvisioningStart?.();
|
|
1035
|
+
let ownTarget;
|
|
1036
|
+
try {
|
|
1037
|
+
ownTarget = await Sync.ensureTelegramLeaderThreadBinding({
|
|
1038
|
+
getAllowedUserId: deps.getAllowedUserId,
|
|
1039
|
+
instanceId: deps.instanceId,
|
|
1040
|
+
cwd: normalizedCwd,
|
|
1041
|
+
sessionId: deps.getSessionId?.(ctx),
|
|
1042
|
+
telegramProfile: deps.getTelegramProfile?.(),
|
|
1043
|
+
forceFreshUnnamed: deps.shouldForceFreshUnnamed?.(),
|
|
1044
|
+
requestedThreadName: deps.getRequestedThreadName?.(),
|
|
1045
|
+
resolveInitialWorkspaceDisplayTitle: deps.resolveInitialWorkspaceDisplayTitle,
|
|
1046
|
+
getNowMs,
|
|
1047
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
1048
|
+
getThreadReconciliationMachineState: deps.getThreadReconciliationMachineState,
|
|
1049
|
+
recordThreadReconciliationPlan: deps.recordThreadReconciliationPlan,
|
|
1050
|
+
topicTargetStore: deps.topicTargetStore,
|
|
1051
|
+
callApi: deps.callApi,
|
|
1052
|
+
probeWorkspaceBinding: async (binding) => {
|
|
1053
|
+
const announcement = createTelegramBusInstanceLifecycleAnnouncement({
|
|
1054
|
+
target: binding.target,
|
|
1055
|
+
threadName: binding.displayTitle ?? binding.threadName,
|
|
1056
|
+
slot: binding.slot,
|
|
1057
|
+
state: "connected",
|
|
1058
|
+
});
|
|
1059
|
+
await deps.callApi("sendMessage", {
|
|
1060
|
+
chat_id: announcement.target.chatId,
|
|
1061
|
+
message_thread_id: announcement.target.threadId,
|
|
1062
|
+
text: announcement.text,
|
|
1063
|
+
parse_mode: announcement.parseMode,
|
|
1064
|
+
});
|
|
1065
|
+
},
|
|
1066
|
+
recordEvent: deps.recordRuntimeEvent,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
finally {
|
|
1070
|
+
deps.onProvisioningEnd?.();
|
|
1071
|
+
}
|
|
1072
|
+
if (deferredOwnCleanups.length > 0) {
|
|
1073
|
+
const supersededCleanupPlan = ThreadReconciler.planThreadReconciliation({
|
|
1074
|
+
nowMs: getNowMs(),
|
|
1075
|
+
currentLeaderEpoch: leaderEpoch,
|
|
1076
|
+
previousState: deps.getThreadReconciliationMachineState?.(),
|
|
1077
|
+
records: deps.topicTargetStore.list(),
|
|
1078
|
+
pendingCleanups: deferredOwnCleanups,
|
|
1079
|
+
});
|
|
1080
|
+
deps.recordThreadReconciliationPlan?.(supersededCleanupPlan);
|
|
1081
|
+
await ThreadReconciler.applyThreadReconciliationPlan(supersededCleanupPlan, cleanupPorts);
|
|
1082
|
+
}
|
|
1083
|
+
if (deps.getCurrentLeaderEpoch &&
|
|
1084
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch) {
|
|
1085
|
+
throw new Error("Telegram leader target provisioning lost ownership.");
|
|
1086
|
+
}
|
|
1087
|
+
if (!ownTarget)
|
|
1088
|
+
return;
|
|
1089
|
+
deps.setLeaderTarget({
|
|
1090
|
+
target: ownTarget.target,
|
|
1091
|
+
slot: ownTarget.slot,
|
|
1092
|
+
threadName: ownTarget.threadName,
|
|
1093
|
+
});
|
|
1094
|
+
const nowMs = getNowMs();
|
|
1095
|
+
let syncState = deps.getSyncState();
|
|
1096
|
+
syncState = Sync.markTelegramSyncSliceFresh(syncState, "target-bindings", {
|
|
1097
|
+
nowMs,
|
|
1098
|
+
action: "leader-startup",
|
|
1099
|
+
});
|
|
1100
|
+
syncState = Sync.markTelegramSyncSliceFresh(syncState, "reservations", {
|
|
1101
|
+
nowMs,
|
|
1102
|
+
action: "leader-startup",
|
|
1103
|
+
});
|
|
1104
|
+
syncState = Sync.markTelegramSyncSliceFresh(syncState, "topic-capability", {
|
|
1105
|
+
nowMs,
|
|
1106
|
+
action: "leader-startup",
|
|
1107
|
+
});
|
|
1108
|
+
deps.setSyncState(syncState);
|
|
1109
|
+
if (ownTarget.reused)
|
|
1110
|
+
return;
|
|
1111
|
+
const connectedAnnouncement = createTelegramBusInstanceLifecycleAnnouncement({
|
|
1112
|
+
target: ownTarget.target,
|
|
1113
|
+
threadName: ownTarget.displayTitle ?? ownTarget.threadName,
|
|
1114
|
+
slot: ownTarget.slot,
|
|
1115
|
+
state: "connected",
|
|
1116
|
+
});
|
|
1117
|
+
try {
|
|
1118
|
+
await deps.callApi("sendMessage", {
|
|
1119
|
+
chat_id: connectedAnnouncement.target.chatId,
|
|
1120
|
+
message_thread_id: connectedAnnouncement.target.threadId,
|
|
1121
|
+
text: connectedAnnouncement.text,
|
|
1122
|
+
parse_mode: connectedAnnouncement.parseMode,
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
catch (error) {
|
|
1126
|
+
deps.recordRuntimeEvent("telegram", error, {
|
|
1127
|
+
phase: "leader-topic-announce",
|
|
1128
|
+
instanceId: deps.instanceId,
|
|
1129
|
+
chatId: ownTarget.target.chatId,
|
|
1130
|
+
threadId: ownTarget.target.threadId,
|
|
1131
|
+
slot: ownTarget.slot,
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
export function createTelegramBusLeaderApiProxy(deps) {
|
|
1137
|
+
return async (method, args) => {
|
|
1138
|
+
if (method === "call") {
|
|
1139
|
+
const body = stripTelegramBusApiMetadata(args[1]);
|
|
1140
|
+
try {
|
|
1141
|
+
return await deps.call(args[0], body, args[2]);
|
|
1142
|
+
}
|
|
1143
|
+
catch (error) {
|
|
1144
|
+
await deps.recoverStaleTargetError?.(body, error);
|
|
1145
|
+
throw error;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (method === "callMultipart") {
|
|
1149
|
+
const fields = args[1];
|
|
1150
|
+
try {
|
|
1151
|
+
return await deps.callMultipart(args[0], fields, args[2], args[3], args[4], args[5]);
|
|
1152
|
+
}
|
|
1153
|
+
catch (error) {
|
|
1154
|
+
await deps.recoverStaleTargetError?.(fields, error);
|
|
1155
|
+
throw error;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (method === "downloadFile") {
|
|
1159
|
+
return deps.downloadFile(args[0], args[1]);
|
|
1160
|
+
}
|
|
1161
|
+
throw new Error(`Unsupported Telegram bus API method: ${method}`);
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
function createTelegramBusFollowerMutationRunner() {
|
|
1165
|
+
const tails = new Map();
|
|
1166
|
+
return async (follower, operation) => {
|
|
1167
|
+
const key = follower.profileKey
|
|
1168
|
+
? `profile:${follower.profileKey}`
|
|
1169
|
+
: `instance:${follower.instanceId}`;
|
|
1170
|
+
const previous = tails.get(key);
|
|
1171
|
+
let release;
|
|
1172
|
+
const current = new Promise((resolve) => {
|
|
1173
|
+
release = resolve;
|
|
1174
|
+
});
|
|
1175
|
+
tails.set(key, current);
|
|
1176
|
+
if (previous)
|
|
1177
|
+
await previous;
|
|
1178
|
+
try {
|
|
1179
|
+
return await operation();
|
|
1180
|
+
}
|
|
1181
|
+
finally {
|
|
1182
|
+
release();
|
|
1183
|
+
if (tails.get(key) === current)
|
|
1184
|
+
tails.delete(key);
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
1189
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1190
|
+
const runFollowerMutation = deps.runFollowerMutation ?? createTelegramBusFollowerMutationRunner();
|
|
1191
|
+
const handleAgentRequest = async (envelope) => {
|
|
1192
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
1193
|
+
if (!follower) {
|
|
1194
|
+
return {
|
|
1195
|
+
kind: "bus.ack",
|
|
1196
|
+
requestId: envelope.requestId,
|
|
1197
|
+
ok: false,
|
|
1198
|
+
message: "Unknown Telegram bus follower instance.",
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
if (!follower.registrationGeneration ||
|
|
1202
|
+
envelope.registrationGeneration !== follower.registrationGeneration) {
|
|
1203
|
+
return {
|
|
1204
|
+
kind: "bus.ack",
|
|
1205
|
+
requestId: envelope.requestId,
|
|
1206
|
+
ok: false,
|
|
1207
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
deps.followerRegistry.heartbeat(envelope.instanceId, getNowMs());
|
|
1211
|
+
if (envelope.kind === "follower.resolveAgentTarget") {
|
|
1212
|
+
const target = await deps.resolveAgentTarget?.(follower, envelope.selector);
|
|
1213
|
+
return target
|
|
1214
|
+
? {
|
|
1215
|
+
kind: "bus.ack",
|
|
1216
|
+
requestId: envelope.requestId,
|
|
1217
|
+
ok: true,
|
|
1218
|
+
result: target,
|
|
1219
|
+
}
|
|
1220
|
+
: {
|
|
1221
|
+
kind: "bus.ack",
|
|
1222
|
+
requestId: envelope.requestId,
|
|
1223
|
+
ok: false,
|
|
1224
|
+
message: "Telegram agent target is unavailable or ambiguous.",
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
if (!deps.routeAgentMessage) {
|
|
1228
|
+
return {
|
|
1229
|
+
kind: "bus.ack",
|
|
1230
|
+
requestId: envelope.requestId,
|
|
1231
|
+
ok: false,
|
|
1232
|
+
message: "Telegram agent message routing is unavailable.",
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
await deps.routeAgentMessage(follower, envelope.message);
|
|
1236
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
|
|
1237
|
+
};
|
|
1238
|
+
const routeQueueHandoff = async (envelope) => {
|
|
1239
|
+
const donor = deps.followerRegistry.get(envelope.instanceId);
|
|
1240
|
+
if (!donor) {
|
|
1241
|
+
return {
|
|
1242
|
+
kind: "bus.ack",
|
|
1243
|
+
requestId: envelope.requestId,
|
|
1244
|
+
ok: false,
|
|
1245
|
+
message: "Unknown Telegram bus follower instance.",
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
if (!donor.registrationGeneration ||
|
|
1249
|
+
envelope.registrationGeneration !== donor.registrationGeneration) {
|
|
1250
|
+
return {
|
|
1251
|
+
kind: "bus.ack",
|
|
1252
|
+
requestId: envelope.requestId,
|
|
1253
|
+
ok: false,
|
|
1254
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
const recipient = deps.followerRegistry.get(envelope.recipientInstanceId);
|
|
1258
|
+
if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) ||
|
|
1259
|
+
!hasTelegramBusCapability(donor.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) ||
|
|
1260
|
+
!hasTelegramBusCapability(recipient?.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF)) {
|
|
1261
|
+
return {
|
|
1262
|
+
kind: "bus.ack",
|
|
1263
|
+
requestId: envelope.requestId,
|
|
1264
|
+
ok: false,
|
|
1265
|
+
message: "Telegram queue handoff capability was not negotiated.",
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
if (!recipient?.registrationGeneration ||
|
|
1269
|
+
envelope.recipientRegistrationGeneration !==
|
|
1270
|
+
recipient.registrationGeneration) {
|
|
1271
|
+
return {
|
|
1272
|
+
kind: "bus.ack",
|
|
1273
|
+
requestId: envelope.requestId,
|
|
1274
|
+
ok: false,
|
|
1275
|
+
message: "Stale Telegram queue handoff recipient registration generation.",
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
if (donor.instanceId === recipient.instanceId) {
|
|
1279
|
+
return {
|
|
1280
|
+
kind: "bus.ack",
|
|
1281
|
+
requestId: envelope.requestId,
|
|
1282
|
+
ok: false,
|
|
1283
|
+
message: "Telegram queue handoff recipient must be another runtime.",
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
if (deps.routeQueueHandoff) {
|
|
1287
|
+
const result = await deps.routeQueueHandoff(donor, envelope);
|
|
1288
|
+
return {
|
|
1289
|
+
kind: "bus.ack",
|
|
1290
|
+
requestId: envelope.requestId,
|
|
1291
|
+
ok: true,
|
|
1292
|
+
...(result !== undefined ? { result } : {}),
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
const recipientSocketPath = recipient.busSocketPath ??
|
|
1296
|
+
getTelegramBusFollowerSocketPath(recipient.instanceId);
|
|
1297
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
1298
|
+
socketPath: recipientSocketPath,
|
|
1299
|
+
timeoutMs: deps.timeoutMs,
|
|
1300
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
1301
|
+
endpoint: recipientSocketPath,
|
|
1302
|
+
operation: "operation",
|
|
1303
|
+
}),
|
|
1304
|
+
envelope: {
|
|
1305
|
+
kind: "leader.offerQueueHandoff",
|
|
1306
|
+
requestId: envelope.requestId,
|
|
1307
|
+
auth: envelope.auth,
|
|
1308
|
+
recipientInstanceId: recipient.instanceId,
|
|
1309
|
+
recipientRegistrationGeneration: recipient.registrationGeneration,
|
|
1310
|
+
donorInstanceId: donor.instanceId,
|
|
1311
|
+
donorProcessId: envelope.donorProcessId,
|
|
1312
|
+
donorProcessBirthId: envelope.donorProcessBirthId,
|
|
1313
|
+
donorSessionGeneration: envelope.donorSessionGeneration,
|
|
1314
|
+
donorAcquisitionId: envelope.donorAcquisitionId,
|
|
1315
|
+
donorAcquiredAtMs: envelope.donorAcquiredAtMs,
|
|
1316
|
+
handoffToken: envelope.handoffToken,
|
|
1317
|
+
payload: envelope.payload,
|
|
1318
|
+
sentAtMs: envelope.sentAtMs,
|
|
1319
|
+
},
|
|
1320
|
+
});
|
|
1321
|
+
if (response?.kind === "bus.ack" && response.ok) {
|
|
1322
|
+
deps.followerRegistry.heartbeat(donor.instanceId, getNowMs());
|
|
1323
|
+
deps.followerRegistry.heartbeat(recipient.instanceId, getNowMs());
|
|
1324
|
+
return {
|
|
1325
|
+
kind: "bus.ack",
|
|
1326
|
+
requestId: envelope.requestId,
|
|
1327
|
+
ok: true,
|
|
1328
|
+
...(response.result !== undefined ? { result: response.result } : {}),
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
return {
|
|
1332
|
+
kind: "bus.ack",
|
|
1333
|
+
requestId: envelope.requestId,
|
|
1334
|
+
ok: false,
|
|
1335
|
+
message: response?.kind === "bus.ack"
|
|
1336
|
+
? response.message
|
|
1337
|
+
: "Telegram queue handoff recipient did not acknowledge staging.",
|
|
1338
|
+
};
|
|
1339
|
+
};
|
|
1340
|
+
const forwardToFollower = async (envelope) => {
|
|
1341
|
+
const follower = deps.followerRegistry.get(envelope.recipientInstanceId);
|
|
1342
|
+
if (!follower) {
|
|
1343
|
+
return {
|
|
1344
|
+
kind: "bus.ack",
|
|
1345
|
+
requestId: envelope.requestId,
|
|
1346
|
+
ok: false,
|
|
1347
|
+
message: "Unknown Telegram bus follower instance.",
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
if (!follower.registrationGeneration ||
|
|
1351
|
+
envelope.recipientRegistrationGeneration !==
|
|
1352
|
+
follower.registrationGeneration) {
|
|
1353
|
+
return {
|
|
1354
|
+
kind: "bus.ack",
|
|
1355
|
+
requestId: envelope.requestId,
|
|
1356
|
+
ok: false,
|
|
1357
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
const followerSocketPath = follower.busSocketPath ??
|
|
1361
|
+
getTelegramBusFollowerSocketPath(envelope.recipientInstanceId);
|
|
1362
|
+
deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
|
|
1363
|
+
try {
|
|
1364
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
1365
|
+
socketPath: followerSocketPath,
|
|
1366
|
+
envelope,
|
|
1367
|
+
timeoutMs: deps.timeoutMs,
|
|
1368
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
1369
|
+
endpoint: followerSocketPath,
|
|
1370
|
+
operation: "operation",
|
|
1371
|
+
}),
|
|
1372
|
+
});
|
|
1373
|
+
if (response?.kind === "bus.ack" && response.ok) {
|
|
1374
|
+
deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
|
|
1375
|
+
return {
|
|
1376
|
+
kind: "bus.ack",
|
|
1377
|
+
requestId: envelope.requestId,
|
|
1378
|
+
ok: true,
|
|
1379
|
+
...(response.result !== undefined ? { result: response.result } : {}),
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
const message = response?.kind === "bus.ack" ? response.message : undefined;
|
|
1383
|
+
return {
|
|
1384
|
+
kind: "bus.ack",
|
|
1385
|
+
requestId: envelope.requestId,
|
|
1386
|
+
ok: false,
|
|
1387
|
+
message: message ?? "Telegram bus follower rejected forwarded update.",
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
catch (error) {
|
|
1391
|
+
return {
|
|
1392
|
+
kind: "bus.ack",
|
|
1393
|
+
requestId: envelope.requestId,
|
|
1394
|
+
ok: false,
|
|
1395
|
+
message: error instanceof Error
|
|
1396
|
+
? error.message
|
|
1397
|
+
: "Telegram bus follower forwarding failed.",
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
return async (envelope) => {
|
|
1402
|
+
const trafficClass = getTelegramBusEnvelopeTrafficClass(envelope);
|
|
1403
|
+
if (trafficClass === "response") {
|
|
1404
|
+
return {
|
|
1405
|
+
kind: "bus.ack",
|
|
1406
|
+
requestId: envelope.requestId,
|
|
1407
|
+
ok: false,
|
|
1408
|
+
message: "Telegram bus response envelope cannot be used as a request.",
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
if (!isTelegramBusEnvelopeAuthorized(envelope, deps.authSecret)) {
|
|
1412
|
+
return createUnauthorizedBusAck(envelope.requestId);
|
|
1413
|
+
}
|
|
1414
|
+
switch (envelope.kind) {
|
|
1415
|
+
case "follower.register":
|
|
1416
|
+
case "follower.restoreWorkspace": {
|
|
1417
|
+
const compatibility = getTelegramBusProtocolCompatibility({
|
|
1418
|
+
local: deps.protocolIdentity,
|
|
1419
|
+
remote: envelope.registration.protocol,
|
|
1420
|
+
});
|
|
1421
|
+
const displayCompatible = () => (deps.getThreadDisplayMode?.() ?? "names") === "names" || (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) &&
|
|
1422
|
+
hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE));
|
|
1423
|
+
const restoringWorkspace = envelope.kind === "follower.restoreWorkspace";
|
|
1424
|
+
const supportsWorkspaceAutoConnect = hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT) &&
|
|
1425
|
+
hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT);
|
|
1426
|
+
if (!compatibility.compatible || !displayCompatible() ||
|
|
1427
|
+
(envelope.registration.cwd !== undefined &&
|
|
1428
|
+
envelope.registration.sessionId === undefined) ||
|
|
1429
|
+
(restoringWorkspace && !supportsWorkspaceAutoConnect)) {
|
|
1430
|
+
return {
|
|
1431
|
+
kind: "bus.ack",
|
|
1432
|
+
requestId: envelope.requestId,
|
|
1433
|
+
ok: false,
|
|
1434
|
+
protocol: deps.protocolIdentity,
|
|
1435
|
+
error: { code: "incompatible-protocol" },
|
|
1436
|
+
message: `Incompatible Telegram bus protocol: ${compatibility.reason ??
|
|
1437
|
+
(envelope.registration.cwd !== undefined &&
|
|
1438
|
+
envelope.registration.sessionId === undefined
|
|
1439
|
+
? "missing-session-identity"
|
|
1440
|
+
: "missing-capability")}.`,
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
const registrationOperation = () => runFollowerMutation(envelope.registration, async () => {
|
|
1444
|
+
try {
|
|
1445
|
+
if (!envelope.registration.registrationGeneration) {
|
|
1446
|
+
throw new Error("Telegram follower registration requires an exact generation.");
|
|
1447
|
+
}
|
|
1448
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
1449
|
+
if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
|
|
1450
|
+
throw new Error("Telegram follower registration requires leader ownership.");
|
|
1451
|
+
}
|
|
1452
|
+
const target = await deps.provisionFollowerTarget?.(envelope.registration, { existingWorkspaceBindingOnly: restoringWorkspace });
|
|
1453
|
+
if (restoringWorkspace && !target) {
|
|
1454
|
+
return {
|
|
1455
|
+
kind: "bus.ack",
|
|
1456
|
+
requestId: envelope.requestId,
|
|
1457
|
+
ok: false,
|
|
1458
|
+
protocol: deps.protocolIdentity,
|
|
1459
|
+
error: { code: "workspace-binding-unavailable" },
|
|
1460
|
+
message: "No remembered Telegram Workspace Thread is available.",
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
if (deps.getCurrentLeaderEpoch &&
|
|
1464
|
+
deps.getCurrentLeaderEpoch() !== leaderEpoch) {
|
|
1465
|
+
throw new Error("Telegram follower registration lost leader ownership.");
|
|
1466
|
+
}
|
|
1467
|
+
if (!displayCompatible()) {
|
|
1468
|
+
throw new Error("Thread display mode changed; update or restart this follower before connecting.");
|
|
1469
|
+
}
|
|
1470
|
+
const registeredTarget = target ?? envelope.registration.target;
|
|
1471
|
+
const registeredSlot = target?.slot ?? envelope.registration.slot;
|
|
1472
|
+
if (deps.provisionFollowerTarget &&
|
|
1473
|
+
registeredTarget?.threadId !== undefined &&
|
|
1474
|
+
(!registeredSlot || !/^[A-Z]$/u.test(registeredSlot))) {
|
|
1475
|
+
throw new Error("Telegram Thread slot authority is unavailable.");
|
|
1476
|
+
}
|
|
1477
|
+
deps.followerRegistry.register({
|
|
1478
|
+
...envelope.registration,
|
|
1479
|
+
connectedAtMs: getNowMs(),
|
|
1480
|
+
target: registeredTarget,
|
|
1481
|
+
...(registeredSlot ? { slot: registeredSlot } : {}),
|
|
1482
|
+
...((target?.threadName ?? envelope.registration.threadName)
|
|
1483
|
+
? {
|
|
1484
|
+
threadName: target?.threadName ?? envelope.registration.threadName,
|
|
1485
|
+
}
|
|
1486
|
+
: {}),
|
|
1487
|
+
});
|
|
1488
|
+
const follower = deps.followerRegistry.get(envelope.registration.instanceId);
|
|
1489
|
+
const displayTitle = follower ? deps.getFollowerDisplayTitle?.(follower) : undefined;
|
|
1490
|
+
deps.onFollowerRegistered?.();
|
|
1491
|
+
return {
|
|
1492
|
+
kind: "bus.ack",
|
|
1493
|
+
requestId: envelope.requestId,
|
|
1494
|
+
ok: true,
|
|
1495
|
+
protocol: deps.protocolIdentity,
|
|
1496
|
+
...(registeredTarget ? {
|
|
1497
|
+
result: {
|
|
1498
|
+
...registeredTarget,
|
|
1499
|
+
...(displayTitle !== undefined ? { displayTitle } : {}),
|
|
1500
|
+
},
|
|
1501
|
+
} : {}),
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
catch (error) {
|
|
1505
|
+
return {
|
|
1506
|
+
kind: "bus.ack",
|
|
1507
|
+
requestId: envelope.requestId,
|
|
1508
|
+
ok: false,
|
|
1509
|
+
protocol: deps.protocolIdentity,
|
|
1510
|
+
message: error instanceof Error
|
|
1511
|
+
? error.message
|
|
1512
|
+
: "Telegram bus follower target provisioning failed.",
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
if (!deps.runWorkspaceAdmission)
|
|
1517
|
+
return registrationOperation();
|
|
1518
|
+
try {
|
|
1519
|
+
return await deps.runWorkspaceAdmission({
|
|
1520
|
+
operationId: `follower-registration:${envelope.requestId}`,
|
|
1521
|
+
operationKind: "workspace.register-follower",
|
|
1522
|
+
scopes: [{ kind: "profile" }],
|
|
1523
|
+
}, registrationOperation);
|
|
1524
|
+
}
|
|
1525
|
+
catch (error) {
|
|
1526
|
+
return {
|
|
1527
|
+
kind: "bus.ack",
|
|
1528
|
+
requestId: envelope.requestId,
|
|
1529
|
+
ok: false,
|
|
1530
|
+
protocol: deps.protocolIdentity,
|
|
1531
|
+
message: error instanceof Error
|
|
1532
|
+
? error.message
|
|
1533
|
+
: "Telegram bus follower registration admission failed.",
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
case "follower.offerQueueHandoff":
|
|
1538
|
+
return routeQueueHandoff(envelope);
|
|
1539
|
+
case "follower.setThreadDisplayMode": {
|
|
1540
|
+
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
1541
|
+
const current = () => epoch !== undefined && deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
1542
|
+
deps.followerRegistry.get(envelope.instanceId)?.registrationGeneration === envelope.registrationGeneration;
|
|
1543
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
1544
|
+
const requiredCapability = envelope.mode === "directory-snake" || envelope.mode === "directory-title"
|
|
1545
|
+
? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
|
|
1546
|
+
: TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
|
|
1547
|
+
if (!current() || !follower?.registrationGeneration || !deps.applyThreadDisplayMode ||
|
|
1548
|
+
!hasTelegramBusCapability(deps.protocolIdentity, requiredCapability) ||
|
|
1549
|
+
!hasTelegramBusCapability(follower.protocol, requiredCapability)) {
|
|
1550
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
|
|
1551
|
+
message: "Thread display settings require current registration and compatible leader authority." };
|
|
1552
|
+
}
|
|
1553
|
+
try {
|
|
1554
|
+
await deps.applyThreadDisplayMode(envelope.mode, current);
|
|
1555
|
+
if (!current())
|
|
1556
|
+
throw new Error("Thread display setting completed for a stale registration.");
|
|
1557
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result: { mode: envelope.mode } };
|
|
1558
|
+
}
|
|
1559
|
+
catch (error) {
|
|
1560
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
|
|
1561
|
+
message: error instanceof Error ? error.message : "Thread display setting was not fully applied." };
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
case "follower.renameThread": {
|
|
1565
|
+
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
1566
|
+
return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
|
|
1567
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
1568
|
+
if (!follower) {
|
|
1569
|
+
return {
|
|
1570
|
+
kind: "bus.ack",
|
|
1571
|
+
requestId: envelope.requestId,
|
|
1572
|
+
ok: false,
|
|
1573
|
+
message: "Unknown Telegram bus follower instance.",
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
if (!follower.registrationGeneration ||
|
|
1577
|
+
envelope.registrationGeneration !== follower.registrationGeneration ||
|
|
1578
|
+
follower.target?.chatId !== envelope.target.chatId ||
|
|
1579
|
+
follower.target?.threadId !== envelope.target.threadId) {
|
|
1580
|
+
return {
|
|
1581
|
+
kind: "bus.ack",
|
|
1582
|
+
requestId: envelope.requestId,
|
|
1583
|
+
ok: false,
|
|
1584
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) ||
|
|
1588
|
+
!hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) ||
|
|
1589
|
+
!deps.renameFollowerThread) {
|
|
1590
|
+
return {
|
|
1591
|
+
kind: "bus.ack",
|
|
1592
|
+
requestId: envelope.requestId,
|
|
1593
|
+
ok: false,
|
|
1594
|
+
message: "Telegram Workspace Thread rename is unavailable.",
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
try {
|
|
1598
|
+
const result = await deps.renameFollowerThread(follower, envelope.threadName);
|
|
1599
|
+
const current = deps.followerRegistry.get(follower.instanceId);
|
|
1600
|
+
if (current?.registrationGeneration !== follower.registrationGeneration) {
|
|
1601
|
+
return {
|
|
1602
|
+
kind: "bus.ack",
|
|
1603
|
+
requestId: envelope.requestId,
|
|
1604
|
+
ok: false,
|
|
1605
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
deps.followerRegistry.register({
|
|
1609
|
+
...current,
|
|
1610
|
+
threadName: result.threadName,
|
|
1611
|
+
connectedAtMs: current.connectedAtMs,
|
|
1612
|
+
});
|
|
1613
|
+
return {
|
|
1614
|
+
kind: "bus.ack",
|
|
1615
|
+
requestId: envelope.requestId,
|
|
1616
|
+
ok: true,
|
|
1617
|
+
result,
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
catch (error) {
|
|
1621
|
+
return {
|
|
1622
|
+
kind: "bus.ack",
|
|
1623
|
+
requestId: envelope.requestId,
|
|
1624
|
+
ok: false,
|
|
1625
|
+
message: error instanceof Error
|
|
1626
|
+
? error.message
|
|
1627
|
+
: "Telegram Workspace Thread rename failed.",
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
case "follower.resetThreadName": {
|
|
1633
|
+
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
1634
|
+
return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
|
|
1635
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
1636
|
+
if (!follower || !follower.registrationGeneration ||
|
|
1637
|
+
envelope.registrationGeneration !== follower.registrationGeneration ||
|
|
1638
|
+
follower.target?.chatId !== envelope.target.chatId ||
|
|
1639
|
+
follower.target?.threadId !== envelope.target.threadId) {
|
|
1640
|
+
return {
|
|
1641
|
+
kind: "bus.ack",
|
|
1642
|
+
requestId: envelope.requestId,
|
|
1643
|
+
ok: false,
|
|
1644
|
+
message: "Stale or unknown Telegram bus follower registration.",
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) || !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) || !deps.resetFollowerThreadName) {
|
|
1648
|
+
return {
|
|
1649
|
+
kind: "bus.ack",
|
|
1650
|
+
requestId: envelope.requestId,
|
|
1651
|
+
ok: false,
|
|
1652
|
+
message: "Telegram Workspace Thread reset is unavailable.",
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
try {
|
|
1656
|
+
const result = await deps.resetFollowerThreadName(follower);
|
|
1657
|
+
const current = deps.followerRegistry.get(follower.instanceId);
|
|
1658
|
+
if (current?.registrationGeneration !==
|
|
1659
|
+
follower.registrationGeneration) {
|
|
1660
|
+
return {
|
|
1661
|
+
kind: "bus.ack",
|
|
1662
|
+
requestId: envelope.requestId,
|
|
1663
|
+
ok: false,
|
|
1664
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
deps.followerRegistry.register({
|
|
1668
|
+
...current,
|
|
1669
|
+
threadName: result.threadName,
|
|
1670
|
+
connectedAtMs: current.connectedAtMs,
|
|
1671
|
+
});
|
|
1672
|
+
return {
|
|
1673
|
+
kind: "bus.ack",
|
|
1674
|
+
requestId: envelope.requestId,
|
|
1675
|
+
ok: true,
|
|
1676
|
+
result,
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
catch (error) {
|
|
1680
|
+
return {
|
|
1681
|
+
kind: "bus.ack",
|
|
1682
|
+
requestId: envelope.requestId,
|
|
1683
|
+
ok: false,
|
|
1684
|
+
message: error instanceof Error
|
|
1685
|
+
? error.message : "Telegram Workspace Thread reset failed.",
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
case "follower.disconnect": {
|
|
1691
|
+
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
1692
|
+
return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
|
|
1693
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
1694
|
+
if (!follower) {
|
|
1695
|
+
return {
|
|
1696
|
+
kind: "bus.ack",
|
|
1697
|
+
requestId: envelope.requestId,
|
|
1698
|
+
ok: false,
|
|
1699
|
+
message: "Unknown Telegram bus follower instance.",
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
if (!follower.registrationGeneration ||
|
|
1703
|
+
!envelope.registrationGeneration ||
|
|
1704
|
+
envelope.registrationGeneration !== follower.registrationGeneration) {
|
|
1705
|
+
return {
|
|
1706
|
+
kind: "bus.ack",
|
|
1707
|
+
requestId: envelope.requestId,
|
|
1708
|
+
ok: false,
|
|
1709
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
await deps.onFollowerDisconnected?.(follower);
|
|
1713
|
+
const current = deps.followerRegistry.get(follower.instanceId);
|
|
1714
|
+
if (current?.registrationGeneration !== follower.registrationGeneration) {
|
|
1715
|
+
return {
|
|
1716
|
+
kind: "bus.ack",
|
|
1717
|
+
requestId: envelope.requestId,
|
|
1718
|
+
ok: false,
|
|
1719
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
deps.followerRegistry.remove(follower.instanceId);
|
|
1723
|
+
return {
|
|
1724
|
+
kind: "bus.ack",
|
|
1725
|
+
requestId: envelope.requestId,
|
|
1726
|
+
ok: true,
|
|
1727
|
+
};
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
case "follower.heartbeat": {
|
|
1731
|
+
const current = deps.followerRegistry.get(envelope.instanceId);
|
|
1732
|
+
if (!current) {
|
|
1733
|
+
return {
|
|
1734
|
+
kind: "bus.ack",
|
|
1735
|
+
requestId: envelope.requestId,
|
|
1736
|
+
ok: false,
|
|
1737
|
+
message: "Unknown Telegram bus follower instance.",
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
if (!current.registrationGeneration ||
|
|
1741
|
+
envelope.registrationGeneration !== current.registrationGeneration) {
|
|
1742
|
+
return {
|
|
1743
|
+
kind: "bus.ack",
|
|
1744
|
+
requestId: envelope.requestId,
|
|
1745
|
+
ok: false,
|
|
1746
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
const follower = deps.followerRegistry.heartbeat(envelope.instanceId, getNowMs());
|
|
1750
|
+
const displayTitle = follower ? deps.getFollowerDisplayTitle?.(follower) : undefined;
|
|
1751
|
+
return follower
|
|
1752
|
+
? {
|
|
1753
|
+
kind: "bus.ack",
|
|
1754
|
+
requestId: envelope.requestId,
|
|
1755
|
+
ok: true,
|
|
1756
|
+
result: {
|
|
1757
|
+
...(displayTitle !== undefined ? { displayTitle } : {}),
|
|
1758
|
+
eligibleElectionSlots: deps.followerRegistry
|
|
1759
|
+
.list()
|
|
1760
|
+
.filter((candidate) => !deps.protocolIdentity.capabilities.includes(TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION) ||
|
|
1761
|
+
candidate.protocol?.capabilities.includes(TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION))
|
|
1762
|
+
.map((candidate) => candidate.slot)
|
|
1763
|
+
.filter((slot) => typeof slot === "string" && /^[A-Z]$/.test(slot))
|
|
1764
|
+
.sort(),
|
|
1765
|
+
},
|
|
1766
|
+
}
|
|
1767
|
+
: {
|
|
1768
|
+
kind: "bus.ack",
|
|
1769
|
+
requestId: envelope.requestId,
|
|
1770
|
+
ok: false,
|
|
1771
|
+
message: "Unknown Telegram bus follower instance.",
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
case "follower.resolveAgentTarget":
|
|
1775
|
+
case "follower.routeAgentMessage":
|
|
1776
|
+
return handleAgentRequest(envelope);
|
|
1777
|
+
case "leader.forwardCallback":
|
|
1778
|
+
case "leader.forwardReaction":
|
|
1779
|
+
case "leader.forwardMessage":
|
|
1780
|
+
case "leader.forwardEditedMessage":
|
|
1781
|
+
return forwardToFollower(envelope);
|
|
1782
|
+
case "follower.callApi":
|
|
1783
|
+
return handleFollowerApiCall(envelope, { ...deps, getNowMs });
|
|
1784
|
+
default:
|
|
1785
|
+
return {
|
|
1786
|
+
kind: "bus.ack",
|
|
1787
|
+
requestId: envelope.requestId,
|
|
1788
|
+
ok: false,
|
|
1789
|
+
message: "Telegram bus envelope is not handled by this leader.",
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
function asRecord(value) {
|
|
1795
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
1796
|
+
? value
|
|
1797
|
+
: undefined;
|
|
1798
|
+
}
|
|
1799
|
+
function asInteger(value) {
|
|
1800
|
+
if (typeof value === "number" && Number.isInteger(value))
|
|
1801
|
+
return value;
|
|
1802
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
1803
|
+
return undefined;
|
|
1804
|
+
const parsed = Number(value);
|
|
1805
|
+
return Number.isInteger(parsed) ? parsed : undefined;
|
|
1806
|
+
}
|
|
1807
|
+
function getFollowerApiMethodAndBody(envelope) {
|
|
1808
|
+
if (envelope.method === "call" || envelope.method === "callMultipart") {
|
|
1809
|
+
return {
|
|
1810
|
+
apiMethod: typeof envelope.args[0] === "string" ? envelope.args[0] : "",
|
|
1811
|
+
body: asRecord(envelope.args[1]),
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
return { apiMethod: envelope.method, body: asRecord(envelope.args[0]) };
|
|
1815
|
+
}
|
|
1816
|
+
function getSentMessageIds(result) {
|
|
1817
|
+
const values = Array.isArray(result) ? result : [result];
|
|
1818
|
+
return values
|
|
1819
|
+
.map((value) => asInteger(asRecord(value)?.message_id))
|
|
1820
|
+
.filter((messageId) => messageId !== undefined);
|
|
1821
|
+
}
|
|
1822
|
+
function recordFollowerApiMessageOwnership(input) {
|
|
1823
|
+
if (!input.record)
|
|
1824
|
+
return;
|
|
1825
|
+
const { apiMethod, body } = getFollowerApiMethodAndBody(input.envelope);
|
|
1826
|
+
if (apiMethod !== "sendMessage" &&
|
|
1827
|
+
apiMethod !== "sendRichMessage" &&
|
|
1828
|
+
apiMethod !== "sendPhoto" &&
|
|
1829
|
+
apiMethod !== "sendDocument" &&
|
|
1830
|
+
apiMethod !== "sendVoice" &&
|
|
1831
|
+
apiMethod !== "sendMediaGroup") {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
const chatId = asInteger(body?.chat_id) ?? input.follower.target?.chatId;
|
|
1835
|
+
if (chatId === undefined)
|
|
1836
|
+
return;
|
|
1837
|
+
const threadId = asInteger(body?.message_thread_id) ?? input.follower.target?.threadId;
|
|
1838
|
+
const target = threadId !== undefined ? { chatId, threadId } : { chatId };
|
|
1839
|
+
for (const messageId of getSentMessageIds(input.result)) {
|
|
1840
|
+
input.record({
|
|
1841
|
+
follower: input.follower,
|
|
1842
|
+
chatId,
|
|
1843
|
+
messageId,
|
|
1844
|
+
target,
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
async function handleFollowerApiCall(envelope, deps) {
|
|
1849
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
1850
|
+
if (!follower) {
|
|
1851
|
+
return {
|
|
1852
|
+
kind: "bus.ack",
|
|
1853
|
+
requestId: envelope.requestId,
|
|
1854
|
+
ok: false,
|
|
1855
|
+
message: "Unknown Telegram bus follower instance.",
|
|
1856
|
+
};
|
|
1857
|
+
}
|
|
1858
|
+
if (!follower.registrationGeneration ||
|
|
1859
|
+
envelope.registrationGeneration !== follower.registrationGeneration) {
|
|
1860
|
+
return {
|
|
1861
|
+
kind: "bus.ack",
|
|
1862
|
+
requestId: envelope.requestId,
|
|
1863
|
+
ok: false,
|
|
1864
|
+
message: "Stale Telegram bus follower registration generation.",
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
deps.followerRegistry.heartbeat(envelope.instanceId, deps.getNowMs());
|
|
1868
|
+
if (deps.authorizeFollowerApiCall &&
|
|
1869
|
+
!deps.authorizeFollowerApiCall({
|
|
1870
|
+
follower,
|
|
1871
|
+
method: envelope.method,
|
|
1872
|
+
args: envelope.args,
|
|
1873
|
+
})) {
|
|
1874
|
+
return {
|
|
1875
|
+
kind: "bus.ack",
|
|
1876
|
+
requestId: envelope.requestId,
|
|
1877
|
+
ok: false,
|
|
1878
|
+
message: "Telegram bus API call is not allowed for this follower.",
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
if (!deps.callApi) {
|
|
1882
|
+
return {
|
|
1883
|
+
kind: "bus.ack",
|
|
1884
|
+
requestId: envelope.requestId,
|
|
1885
|
+
ok: false,
|
|
1886
|
+
message: "Telegram bus leader does not expose API calling.",
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
try {
|
|
1890
|
+
const result = await deps.callApi(envelope.method, envelope.args);
|
|
1891
|
+
recordFollowerApiMessageOwnership({
|
|
1892
|
+
envelope,
|
|
1893
|
+
follower,
|
|
1894
|
+
result,
|
|
1895
|
+
record: deps.recordFollowerMessageOwnership,
|
|
1896
|
+
});
|
|
1897
|
+
return {
|
|
1898
|
+
kind: "bus.ack",
|
|
1899
|
+
requestId: envelope.requestId,
|
|
1900
|
+
ok: true,
|
|
1901
|
+
result,
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
catch (error) {
|
|
1905
|
+
const staleTarget = Threads.isTelegramTopicTargetStaleError(error)
|
|
1906
|
+
? getTelegramApiErrorRequestTarget(error)
|
|
1907
|
+
: undefined;
|
|
1908
|
+
return {
|
|
1909
|
+
kind: "bus.ack",
|
|
1910
|
+
requestId: envelope.requestId,
|
|
1911
|
+
ok: false,
|
|
1912
|
+
message: error instanceof Error
|
|
1913
|
+
? error.message
|
|
1914
|
+
: "Telegram bus API call failed.",
|
|
1915
|
+
...(staleTarget
|
|
1916
|
+
? {
|
|
1917
|
+
error: {
|
|
1918
|
+
code: "stale-target",
|
|
1919
|
+
chatId: staleTarget.chatId,
|
|
1920
|
+
threadId: staleTarget.threadId,
|
|
1921
|
+
},
|
|
1922
|
+
}
|
|
1923
|
+
: isTelegramApiCommitUnknownError(error)
|
|
1924
|
+
? {
|
|
1925
|
+
error: {
|
|
1926
|
+
code: "commit-unknown",
|
|
1927
|
+
method: error.method,
|
|
1928
|
+
},
|
|
1929
|
+
}
|
|
1930
|
+
: {}),
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
export function createTelegramBusLeaderActivationScheduler(deps) {
|
|
1935
|
+
let pending = false;
|
|
1936
|
+
return (ctx) => {
|
|
1937
|
+
if (deps.isBusPollingStarted() || pending)
|
|
1938
|
+
return;
|
|
1939
|
+
if (!deps.isBusEnabled())
|
|
1940
|
+
return;
|
|
1941
|
+
if (!deps.ownsPolling(ctx))
|
|
1942
|
+
return;
|
|
1943
|
+
pending = true;
|
|
1944
|
+
const timer = setTimeout(() => {
|
|
1945
|
+
void (async () => {
|
|
1946
|
+
try {
|
|
1947
|
+
if (deps.isBusPollingStarted())
|
|
1948
|
+
return;
|
|
1949
|
+
if (!deps.isBusEnabled())
|
|
1950
|
+
return;
|
|
1951
|
+
if (!deps.ownsPolling(ctx))
|
|
1952
|
+
return;
|
|
1953
|
+
await deps.stopClassicPolling();
|
|
1954
|
+
try {
|
|
1955
|
+
await deps.startBusLeaderPolling(ctx);
|
|
1956
|
+
deps.setBusPollingStarted(true);
|
|
1957
|
+
deps.updateStatus(ctx);
|
|
1958
|
+
deps.recordRuntimeEvent?.("bus", "Telegram bus leader mode activated", { phase: "leader-hot-switch" });
|
|
1959
|
+
}
|
|
1960
|
+
catch (error) {
|
|
1961
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
1962
|
+
phase: "leader-hot-switch",
|
|
1963
|
+
});
|
|
1964
|
+
deps.setBusPollingStarted(false);
|
|
1965
|
+
await deps.startClassicPolling(ctx);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
finally {
|
|
1969
|
+
pending = false;
|
|
1970
|
+
}
|
|
1971
|
+
})();
|
|
1972
|
+
}, 0);
|
|
1973
|
+
timer.unref?.();
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
export function createTelegramBusLeaderRuntime(deps) {
|
|
1977
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1978
|
+
const followerPruneIntervalMs = deps.followerPruneIntervalMs ?? 1000;
|
|
1979
|
+
const followerStaleAfterMs = deps.followerStaleAfterMs ?? TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
|
|
1980
|
+
const runFollowerMutation = createTelegramBusFollowerMutationRunner();
|
|
1981
|
+
let pruneInterval;
|
|
1982
|
+
let pruneGeneration = 0;
|
|
1983
|
+
let prunePromise;
|
|
1984
|
+
const stopPruning = () => {
|
|
1985
|
+
pruneGeneration += 1;
|
|
1986
|
+
if (pruneInterval)
|
|
1987
|
+
clearInterval(pruneInterval);
|
|
1988
|
+
pruneInterval = undefined;
|
|
1989
|
+
prunePromise = undefined;
|
|
1990
|
+
};
|
|
1991
|
+
const recordPruneEvent = (error, details) => {
|
|
1992
|
+
try {
|
|
1993
|
+
deps.recordRuntimeEvent?.("bus", error, details);
|
|
1994
|
+
}
|
|
1995
|
+
catch {
|
|
1996
|
+
// Prune diagnostics cannot replace lifecycle-owned reconciliation.
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
const pruneFollowers = async (expectedGeneration) => {
|
|
2000
|
+
const isCurrent = () => pruneGeneration === expectedGeneration;
|
|
2001
|
+
try {
|
|
2002
|
+
await localServer.ensureEndpoint();
|
|
2003
|
+
}
|
|
2004
|
+
catch (error) {
|
|
2005
|
+
recordPruneEvent(error, {
|
|
2006
|
+
phase: "leader-endpoint-recovery",
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
if (!isCurrent())
|
|
2010
|
+
return;
|
|
2011
|
+
const removed = deps.followerRegistry.pruneStale(getNowMs(), followerStaleAfterMs);
|
|
2012
|
+
for (const follower of removed) {
|
|
2013
|
+
let processConfirmedDead = false;
|
|
2014
|
+
if (follower.pid !== undefined && deps.isFollowerProcessAlive) {
|
|
2015
|
+
try {
|
|
2016
|
+
processConfirmedDead = !deps.isFollowerProcessAlive(follower.pid);
|
|
2017
|
+
}
|
|
2018
|
+
catch (error) {
|
|
2019
|
+
recordPruneEvent(error, {
|
|
2020
|
+
phase: "follower-process-liveness",
|
|
2021
|
+
instanceId: follower.instanceId,
|
|
2022
|
+
pid: follower.pid,
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
if (!processConfirmedDead) {
|
|
2027
|
+
recordPruneEvent("Telegram bus follower heartbeat stale; preserving thread binding", {
|
|
2028
|
+
phase: "follower-pruned",
|
|
2029
|
+
instanceId: follower.instanceId,
|
|
2030
|
+
processLiveness: follower.pid === undefined || !deps.isFollowerProcessAlive
|
|
2031
|
+
? "unknown"
|
|
2032
|
+
: "alive-or-unknown",
|
|
2033
|
+
});
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
2036
|
+
let cleanupEnabled = false;
|
|
2037
|
+
try {
|
|
2038
|
+
cleanupEnabled =
|
|
2039
|
+
(await deps.shouldCleanupConfirmedDeadFollower?.()) ?? false;
|
|
2040
|
+
}
|
|
2041
|
+
catch (error) {
|
|
2042
|
+
recordPruneEvent(error, {
|
|
2043
|
+
phase: "follower-confirmed-dead-cleanup-policy",
|
|
2044
|
+
instanceId: follower.instanceId,
|
|
2045
|
+
pid: follower.pid,
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
if (!isCurrent())
|
|
2049
|
+
return;
|
|
2050
|
+
if (!cleanupEnabled || !deps.onFollowerConfirmedDead) {
|
|
2051
|
+
recordPruneEvent("Telegram bus follower process confirmed dead; preserving thread binding", {
|
|
2052
|
+
phase: "follower-confirmed-dead-preserved",
|
|
2053
|
+
instanceId: follower.instanceId,
|
|
2054
|
+
pid: follower.pid,
|
|
2055
|
+
cleanupEnabled,
|
|
2056
|
+
});
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
try {
|
|
2060
|
+
await runFollowerMutation(follower, async () => {
|
|
2061
|
+
if (!isCurrent())
|
|
2062
|
+
return;
|
|
2063
|
+
const replacement = deps.followerRegistry.list().find((candidate) => follower.profileKey
|
|
2064
|
+
? candidate.profileKey === follower.profileKey
|
|
2065
|
+
: candidate.instanceId === follower.instanceId);
|
|
2066
|
+
if (replacement) {
|
|
2067
|
+
recordPruneEvent("Telegram bus follower replaced before confirmed-dead cleanup; preserving thread binding", {
|
|
2068
|
+
phase: "follower-confirmed-dead-replaced",
|
|
2069
|
+
instanceId: follower.instanceId,
|
|
2070
|
+
replacementInstanceId: replacement.instanceId,
|
|
2071
|
+
});
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
await deps.onFollowerConfirmedDead(follower);
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
catch (error) {
|
|
2078
|
+
recordPruneEvent(error, {
|
|
2079
|
+
phase: "follower-confirmed-dead-cleanup",
|
|
2080
|
+
instanceId: follower.instanceId,
|
|
2081
|
+
pid: follower.pid,
|
|
2082
|
+
chatId: follower.target?.chatId,
|
|
2083
|
+
threadId: follower.target?.threadId,
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
};
|
|
2088
|
+
const requestPrune = () => {
|
|
2089
|
+
if (prunePromise)
|
|
2090
|
+
return prunePromise;
|
|
2091
|
+
const expectedGeneration = pruneGeneration;
|
|
2092
|
+
let tracked;
|
|
2093
|
+
tracked = pruneFollowers(expectedGeneration)
|
|
2094
|
+
.catch((error) => {
|
|
2095
|
+
if (pruneGeneration === expectedGeneration) {
|
|
2096
|
+
recordPruneEvent(error, { phase: "follower-prune-owner" });
|
|
2097
|
+
}
|
|
2098
|
+
})
|
|
2099
|
+
.finally(() => {
|
|
2100
|
+
if (prunePromise === tracked)
|
|
2101
|
+
prunePromise = undefined;
|
|
2102
|
+
});
|
|
2103
|
+
prunePromise = tracked;
|
|
2104
|
+
return tracked;
|
|
2105
|
+
};
|
|
2106
|
+
const startPruning = () => {
|
|
2107
|
+
stopPruning();
|
|
2108
|
+
pruneInterval = setInterval(() => {
|
|
2109
|
+
void requestPrune();
|
|
2110
|
+
}, followerPruneIntervalMs);
|
|
2111
|
+
pruneInterval.unref?.();
|
|
2112
|
+
};
|
|
2113
|
+
const handleEnvelope = createTelegramBusLeaderEnvelopeHandler({
|
|
2114
|
+
followerRegistry: deps.followerRegistry,
|
|
2115
|
+
authSecret: deps.authSecret,
|
|
2116
|
+
protocolIdentity: deps.protocolIdentity,
|
|
2117
|
+
getNowMs,
|
|
2118
|
+
callApi: deps.callApi,
|
|
2119
|
+
authorizeFollowerApiCall: deps.authorizeFollowerApiCall,
|
|
2120
|
+
recordFollowerMessageOwnership: deps.recordFollowerMessageOwnership,
|
|
2121
|
+
resolveAgentTarget: deps.resolveAgentTarget,
|
|
2122
|
+
routeAgentMessage: deps.routeAgentMessage,
|
|
2123
|
+
routeQueueHandoff: deps.routeQueueHandoff,
|
|
2124
|
+
provisionFollowerTarget: deps.provisionFollowerTarget,
|
|
2125
|
+
onFollowerDisconnected: deps.onFollowerDisconnected,
|
|
2126
|
+
renameFollowerThread: deps.renameFollowerThread,
|
|
2127
|
+
resetFollowerThreadName: deps.resetFollowerThreadName,
|
|
2128
|
+
getFollowerDisplayTitle: deps.getFollowerDisplayTitle,
|
|
2129
|
+
onFollowerRegistered: deps.onFollowerRegistered,
|
|
2130
|
+
applyThreadDisplayMode: deps.applyThreadDisplayMode,
|
|
2131
|
+
getThreadDisplayMode: deps.getThreadDisplayMode,
|
|
2132
|
+
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
2133
|
+
runFollowerMutation,
|
|
2134
|
+
runWorkspaceAdmission: deps.runWorkspaceAdmission,
|
|
2135
|
+
});
|
|
2136
|
+
const routeQueueHandoffEnvelope = async (input) => {
|
|
2137
|
+
const recipient = deps.followerRegistry.get(input.recipientInstanceId);
|
|
2138
|
+
if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) ||
|
|
2139
|
+
!hasTelegramBusCapability(recipient?.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF)) {
|
|
2140
|
+
return {
|
|
2141
|
+
kind: "bus.ack",
|
|
2142
|
+
requestId: input.requestId,
|
|
2143
|
+
ok: false,
|
|
2144
|
+
message: "Telegram queue handoff capability was not negotiated.",
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
if (!recipient?.registrationGeneration ||
|
|
2148
|
+
input.recipientRegistrationGeneration !==
|
|
2149
|
+
recipient.registrationGeneration) {
|
|
2150
|
+
return {
|
|
2151
|
+
kind: "bus.ack",
|
|
2152
|
+
requestId: input.requestId,
|
|
2153
|
+
ok: false,
|
|
2154
|
+
message: "Stale Telegram queue handoff recipient registration generation.",
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
const recipientSocketPath = recipient.busSocketPath ??
|
|
2158
|
+
getTelegramBusFollowerSocketPath(recipient.instanceId);
|
|
2159
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
2160
|
+
socketPath: recipientSocketPath,
|
|
2161
|
+
timeoutMs: deps.timeoutMs,
|
|
2162
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
2163
|
+
endpoint: recipientSocketPath,
|
|
2164
|
+
operation: "operation",
|
|
2165
|
+
}),
|
|
2166
|
+
envelope: {
|
|
2167
|
+
kind: "leader.offerQueueHandoff",
|
|
2168
|
+
requestId: input.requestId,
|
|
2169
|
+
auth: input.auth,
|
|
2170
|
+
recipientInstanceId: recipient.instanceId,
|
|
2171
|
+
recipientRegistrationGeneration: recipient.registrationGeneration,
|
|
2172
|
+
donorInstanceId: input.donorInstanceId,
|
|
2173
|
+
donorProcessId: input.donorProcessId,
|
|
2174
|
+
donorProcessBirthId: input.donorProcessBirthId,
|
|
2175
|
+
donorSessionGeneration: input.donorSessionGeneration,
|
|
2176
|
+
donorAcquisitionId: input.donorAcquisitionId,
|
|
2177
|
+
donorAcquiredAtMs: input.donorAcquiredAtMs,
|
|
2178
|
+
handoffToken: input.handoffToken,
|
|
2179
|
+
payload: input.payload,
|
|
2180
|
+
sentAtMs: input.sentAtMs,
|
|
2181
|
+
},
|
|
2182
|
+
});
|
|
2183
|
+
if (response?.kind === "bus.ack" && response.ok) {
|
|
2184
|
+
deps.followerRegistry.heartbeat(recipient.instanceId, getNowMs());
|
|
2185
|
+
return {
|
|
2186
|
+
kind: "bus.ack",
|
|
2187
|
+
requestId: input.requestId,
|
|
2188
|
+
ok: true,
|
|
2189
|
+
...(response.result !== undefined ? { result: response.result } : {}),
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
return {
|
|
2193
|
+
kind: "bus.ack",
|
|
2194
|
+
requestId: input.requestId,
|
|
2195
|
+
ok: false,
|
|
2196
|
+
message: response?.kind === "bus.ack"
|
|
2197
|
+
? response.message
|
|
2198
|
+
: "Telegram queue handoff recipient did not acknowledge staging.",
|
|
2199
|
+
};
|
|
2200
|
+
};
|
|
2201
|
+
const localServer = createTelegramBusLocalServer({
|
|
2202
|
+
socketPath: deps.socketPath,
|
|
2203
|
+
commitEndpointPublication: deps.commitEndpointPublication,
|
|
2204
|
+
recordTransportEvent(phase, details) {
|
|
2205
|
+
deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
|
|
2206
|
+
phase: `leader-${phase}`,
|
|
2207
|
+
...details,
|
|
2208
|
+
});
|
|
2209
|
+
},
|
|
2210
|
+
handleEnvelope,
|
|
2211
|
+
});
|
|
2212
|
+
return {
|
|
2213
|
+
routeQueueHandoff: (envelope) => routeQueueHandoffEnvelope(envelope),
|
|
2214
|
+
startPolling: async (ctx) => {
|
|
2215
|
+
// Replay durable cleanup before publishing the follower endpoint so a
|
|
2216
|
+
// replacement registration cannot reclaim a target while it is deleted.
|
|
2217
|
+
await deps.provisionLeaderTarget?.(ctx);
|
|
2218
|
+
await localServer.start();
|
|
2219
|
+
startPruning();
|
|
2220
|
+
try {
|
|
2221
|
+
await deps.startPolling(ctx);
|
|
2222
|
+
}
|
|
2223
|
+
catch (error) {
|
|
2224
|
+
stopPruning();
|
|
2225
|
+
await localServer.stop();
|
|
2226
|
+
throw error;
|
|
2227
|
+
}
|
|
2228
|
+
},
|
|
2229
|
+
stopPolling: async () => {
|
|
2230
|
+
stopPruning();
|
|
2231
|
+
try {
|
|
2232
|
+
await deps.stopPolling();
|
|
2233
|
+
}
|
|
2234
|
+
finally {
|
|
2235
|
+
await localServer
|
|
2236
|
+
.stop()
|
|
2237
|
+
.catch((error) => deps.recordRuntimeEvent?.("bus", error, { phase: "stop" }));
|
|
2238
|
+
deps.followerRegistry.clear();
|
|
2239
|
+
}
|
|
2240
|
+
},
|
|
2241
|
+
};
|
|
2242
|
+
}
|