@llblab/pi-telegram 0.52.2 → 0.53.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 +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +11 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +61 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +249 -126
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +125 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +511 -300
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
package/lib/bus.ts
CHANGED
|
@@ -5,28 +5,27 @@
|
|
|
5
5
|
* cross-instance forwarding helpers, and the live follower registry model.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { execFileSync } from "node:child_process";
|
|
9
8
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
10
9
|
import {
|
|
11
10
|
chmodSync,
|
|
12
11
|
existsSync,
|
|
13
12
|
lstatSync,
|
|
14
13
|
mkdirSync,
|
|
15
|
-
readFileSync,
|
|
16
14
|
readlinkSync,
|
|
17
15
|
renameSync,
|
|
18
16
|
symlinkSync,
|
|
19
17
|
unlinkSync,
|
|
20
18
|
} from "node:fs";
|
|
19
|
+
import { createRequire } from "node:module";
|
|
21
20
|
import {
|
|
22
21
|
createConnection,
|
|
23
22
|
createServer,
|
|
24
23
|
type Server,
|
|
25
24
|
type Socket,
|
|
26
25
|
} from "node:net";
|
|
27
|
-
import { createRequire } from "node:module";
|
|
28
26
|
import { platform as getPlatform, tmpdir } from "node:os";
|
|
29
27
|
import { basename, dirname, join } from "node:path";
|
|
28
|
+
import { isDeepStrictEqual } from "node:util";
|
|
30
29
|
|
|
31
30
|
import {
|
|
32
31
|
classifyTelegramBusTransportError,
|
|
@@ -37,28 +36,33 @@ import {
|
|
|
37
36
|
getTelegramBusLeaderEndpoint,
|
|
38
37
|
getTelegramBusPipePath,
|
|
39
38
|
getTelegramBusTransportRetryPolicy,
|
|
40
|
-
type TelegramBusEndpointLayout,
|
|
41
|
-
TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES,
|
|
42
|
-
isTelegramBusPipePath,
|
|
43
39
|
isRetryableTelegramBusTransportError,
|
|
40
|
+
isTelegramBusPipePath,
|
|
44
41
|
probeTelegramBusEndpoint,
|
|
42
|
+
TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES,
|
|
43
|
+
type TelegramBusEndpointLayout,
|
|
45
44
|
type TelegramBusTransportEventRecorder,
|
|
46
45
|
type TelegramBusTransportRetryPolicy,
|
|
47
46
|
} from "./bus-transport.ts";
|
|
47
|
+
import type { TelegramThreadDisplayMode } from "./config.ts";
|
|
48
|
+
import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS } from "./locks.ts";
|
|
49
|
+
import { resolveAgentDir } from "./paths.ts";
|
|
50
|
+
import { getTelegramProcessBirthIdentity } from "./process-identity.ts";
|
|
48
51
|
import {
|
|
49
52
|
parseTelegramQueueHandoffPayload,
|
|
50
53
|
TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES,
|
|
51
54
|
type TelegramQueueHandoffPayload,
|
|
52
55
|
} from "./queue.ts";
|
|
53
|
-
import
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
import {
|
|
57
|
+
areTelegramTargetsEqual,
|
|
58
|
+
parseTelegramTarget as parseTarget,
|
|
59
|
+
type TelegramTarget,
|
|
60
|
+
} from "./target.ts";
|
|
58
61
|
import {
|
|
59
62
|
normalizeTelegramSessionReplacementIntent,
|
|
60
63
|
type TelegramSessionReplacementIntent,
|
|
61
64
|
} from "./threads.ts";
|
|
65
|
+
import { hasOnlyWireKeys, isWireRecord as isRecord } from "./wire.ts";
|
|
62
66
|
|
|
63
67
|
interface TelegramBusProcessRuntime {
|
|
64
68
|
instanceId: string;
|
|
@@ -69,113 +73,6 @@ interface TelegramBusProcessRuntime {
|
|
|
69
73
|
getFollowerSocketPath: () => string;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
interface TelegramProcessBirthIdentityOptions {
|
|
73
|
-
platform?: NodeJS.Platform;
|
|
74
|
-
readProcStat?: (pid: number) => string;
|
|
75
|
-
readDarwinProcessStart?: (pid: number) => string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
type TelegramProcessBirthProof =
|
|
79
|
-
| { status: "proven"; identity: string }
|
|
80
|
-
| { status: "unverifiable" };
|
|
81
|
-
|
|
82
|
-
export type TelegramProcessLiveness = "alive" | "dead" | "unverifiable";
|
|
83
|
-
|
|
84
|
-
interface TelegramProcessLivenessOptions
|
|
85
|
-
extends TelegramProcessBirthIdentityOptions {
|
|
86
|
-
isProcessAlive?: (pid: number) => boolean;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function readDarwinProcessStart(pid: number): string {
|
|
90
|
-
return execFileSync(
|
|
91
|
-
"/bin/ps",
|
|
92
|
-
["-o", "lstart=", "-p", String(pid)],
|
|
93
|
-
{
|
|
94
|
-
encoding: "utf8",
|
|
95
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
96
|
-
},
|
|
97
|
-
).trim();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function getTelegramProcessBirthProof(
|
|
101
|
-
pid: number,
|
|
102
|
-
options: TelegramProcessBirthIdentityOptions = {},
|
|
103
|
-
): TelegramProcessBirthProof {
|
|
104
|
-
if (pid <= 0) return { status: "unverifiable" };
|
|
105
|
-
const platform = options.platform ?? getPlatform();
|
|
106
|
-
if (platform === "linux") {
|
|
107
|
-
try {
|
|
108
|
-
const stat = (options.readProcStat ?? ((targetPid) =>
|
|
109
|
-
readFileSync(`/proc/${targetPid}/stat`, "utf8")))(pid);
|
|
110
|
-
const closeParen = stat.lastIndexOf(")");
|
|
111
|
-
const fields = stat
|
|
112
|
-
.slice(closeParen + 2)
|
|
113
|
-
.trim()
|
|
114
|
-
.split(/\s+/u);
|
|
115
|
-
const startTicks = fields[19];
|
|
116
|
-
if (startTicks) {
|
|
117
|
-
return { status: "proven", identity: `${pid}:start:${startTicks}` };
|
|
118
|
-
}
|
|
119
|
-
} catch {
|
|
120
|
-
/* inaccessible process metadata */
|
|
121
|
-
}
|
|
122
|
-
} else if (platform === "darwin") {
|
|
123
|
-
try {
|
|
124
|
-
const startedAt = (
|
|
125
|
-
options.readDarwinProcessStart ?? readDarwinProcessStart
|
|
126
|
-
)(pid);
|
|
127
|
-
if (startedAt) {
|
|
128
|
-
const fingerprint = createHash("sha256")
|
|
129
|
-
.update(startedAt)
|
|
130
|
-
.digest("hex")
|
|
131
|
-
.slice(0, 16);
|
|
132
|
-
return { status: "proven", identity: `${pid}:start:${fingerprint}` };
|
|
133
|
-
}
|
|
134
|
-
} catch {
|
|
135
|
-
/* inaccessible process metadata */
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return { status: "unverifiable" };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export function getTelegramProcessBirthIdentity(
|
|
142
|
-
pid: number,
|
|
143
|
-
fallbackGeneration: number | string,
|
|
144
|
-
options: TelegramProcessBirthIdentityOptions = {},
|
|
145
|
-
): string {
|
|
146
|
-
const proof = getTelegramProcessBirthProof(pid, options);
|
|
147
|
-
return proof.status === "proven"
|
|
148
|
-
? proof.identity
|
|
149
|
-
: `${pid}:generation:${fallbackGeneration}`;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function getTelegramProcessLiveness(
|
|
153
|
-
owner: { processId: number; processBirthId: string },
|
|
154
|
-
options: TelegramProcessLivenessOptions = {},
|
|
155
|
-
): TelegramProcessLiveness {
|
|
156
|
-
const processAlive = options.isProcessAlive ?? isProcessAlive;
|
|
157
|
-
if (!processAlive(owner.processId)) return "dead";
|
|
158
|
-
const proof = getTelegramProcessBirthProof(owner.processId, options);
|
|
159
|
-
if (proof.status === "unverifiable") return "unverifiable";
|
|
160
|
-
return proof.identity === owner.processBirthId ? "alive" : "dead";
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function getTelegramProcessBirthIdentityLiveness(
|
|
164
|
-
processBirthId: string,
|
|
165
|
-
options: TelegramProcessLivenessOptions = {},
|
|
166
|
-
): TelegramProcessLiveness {
|
|
167
|
-
const match = /^(\d+):(start|generation):(.+)$/u.exec(processBirthId);
|
|
168
|
-
if (!match) return "unverifiable";
|
|
169
|
-
const processId = Number(match[1]);
|
|
170
|
-
if (!Number.isSafeInteger(processId) || processId <= 0) return "unverifiable";
|
|
171
|
-
const processAlive = options.isProcessAlive ?? isProcessAlive;
|
|
172
|
-
if (!processAlive(processId)) return "dead";
|
|
173
|
-
if (match[2] === "generation") return "unverifiable";
|
|
174
|
-
const proof = getTelegramProcessBirthProof(processId, options);
|
|
175
|
-
if (proof.status === "unverifiable") return "unverifiable";
|
|
176
|
-
return proof.identity === processBirthId ? "alive" : "dead";
|
|
177
|
-
}
|
|
178
|
-
|
|
179
76
|
export function createCurrentTelegramBusProcessRuntime(input: {
|
|
180
77
|
getActiveProfileName: () => string | undefined;
|
|
181
78
|
endpointLayout?: TelegramBusEndpointLayout;
|
|
@@ -200,7 +97,8 @@ export function createTelegramBusProcessRuntime(input: {
|
|
|
200
97
|
parentProcessIdentity?: string;
|
|
201
98
|
createdAtMs: number;
|
|
202
99
|
}): TelegramBusProcessRuntime {
|
|
203
|
-
const getActiveProfileName = input.getActiveProfileName,
|
|
100
|
+
const getActiveProfileName = input.getActiveProfileName,
|
|
101
|
+
endpointLayout = input.endpointLayout;
|
|
204
102
|
const instanceId = `${input.pid}:${input.createdAtMs}`;
|
|
205
103
|
const ownerPid = input.parentPid || input.pid;
|
|
206
104
|
const manualFollowerOwnerId =
|
|
@@ -242,18 +140,254 @@ export const TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE =
|
|
|
242
140
|
"input-custody-reference-v1" as const;
|
|
243
141
|
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME =
|
|
244
142
|
"workspace-thread-rename-v1" as const;
|
|
245
|
-
export const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE =
|
|
143
|
+
export const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE =
|
|
144
|
+
"thread-display-mode-v1" as const;
|
|
246
145
|
export const TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT =
|
|
247
146
|
"directory-display-format-v1" as const;
|
|
248
147
|
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT =
|
|
249
148
|
"workspace-follower-auto-connect-v1" as const;
|
|
250
149
|
export const TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT =
|
|
251
150
|
"session-replacement-intent-v1" as const;
|
|
252
|
-
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE =
|
|
151
|
+
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE =
|
|
152
|
+
"workspace-restore-v1" as const;
|
|
153
|
+
export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE =
|
|
154
|
+
"live-thread-rebind-save-v1" as const;
|
|
155
|
+
export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY =
|
|
156
|
+
"live-thread-rebind-apply-v1" as const;
|
|
157
|
+
export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE =
|
|
158
|
+
"live-thread-rebind-settle-v1" as const;
|
|
159
|
+
/** Restricted recipient text/menu delivery; negotiated identity and captured effect authority remain required. */
|
|
160
|
+
export const TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY =
|
|
161
|
+
"selected-menu-delivery-v1" as const;
|
|
162
|
+
/** Versioned held-command set; syntax is shared, while the captured recipient Commands registry owns supported plans. */
|
|
163
|
+
export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET =
|
|
164
|
+
"live-thread-rebind-command-set-v1" as const;
|
|
165
|
+
/** Every live rebinding needs the save/apply/settle trio; held command selections additionally need these two. */
|
|
166
|
+
export const TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES = [
|
|
167
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
|
|
168
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
169
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
170
|
+
] as const;
|
|
171
|
+
export const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES = [
|
|
172
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
173
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
174
|
+
] as const;
|
|
175
|
+
|
|
176
|
+
export interface TelegramBusSelectedCommandInput {
|
|
177
|
+
name: string;
|
|
178
|
+
target: TelegramTarget & { threadId: number };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseSelectedCommandInput(
|
|
182
|
+
value: unknown,
|
|
183
|
+
): TelegramBusSelectedCommandInput | undefined {
|
|
184
|
+
if (
|
|
185
|
+
!isRecord(value) ||
|
|
186
|
+
!hasOnlyWireKeys(value, ["name", "target"]) ||
|
|
187
|
+
typeof value.name !== "string" ||
|
|
188
|
+
!/^[a-z0-9_]{1,32}$/u.test(value.name) ||
|
|
189
|
+
!isRecord(value.target) ||
|
|
190
|
+
!hasOnlyWireKeys(value.target, ["chatId", "threadId"])
|
|
191
|
+
)
|
|
192
|
+
return undefined;
|
|
193
|
+
const target = parseThreadTarget(value.target);
|
|
194
|
+
return target &&
|
|
195
|
+
Number.isSafeInteger(target.chatId) &&
|
|
196
|
+
target.chatId > 0 &&
|
|
197
|
+
Number.isSafeInteger(target.threadId) &&
|
|
198
|
+
target.threadId > 0
|
|
199
|
+
? { name: value.name, target }
|
|
200
|
+
: undefined;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export type TelegramBusSelectedMenuTextEffect = {
|
|
204
|
+
text: string;
|
|
205
|
+
parseMode?: "HTML";
|
|
206
|
+
replyMarkup?: {
|
|
207
|
+
inline_keyboard: { text: string; callback_data: string }[][];
|
|
208
|
+
};
|
|
209
|
+
} & (
|
|
210
|
+
| { kind: "send-text"; replyToMessageId?: number }
|
|
211
|
+
| { kind: "edit-text"; messageId: number }
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
/** Serializable identity/effect description, never a process-local callback or standalone delivery grant. */
|
|
215
|
+
export interface TelegramBusSelectedMenuDelivery {
|
|
216
|
+
kind: "follower.deliverSelectedMenu";
|
|
217
|
+
requestId: string;
|
|
218
|
+
instanceId: string;
|
|
219
|
+
registrationGeneration: string;
|
|
220
|
+
operationId: string;
|
|
221
|
+
recipient: {
|
|
222
|
+
sessionId: string;
|
|
223
|
+
sessionGeneration: number;
|
|
224
|
+
processId: number;
|
|
225
|
+
processBirthId: string;
|
|
226
|
+
profileKey: string;
|
|
227
|
+
journalBindingKey: string;
|
|
228
|
+
target: TelegramTarget & { threadId: number };
|
|
229
|
+
};
|
|
230
|
+
executor: { instanceId: string; leaderEpoch: string };
|
|
231
|
+
operatorUserId: number;
|
|
232
|
+
effect: TelegramBusSelectedMenuTextEffect;
|
|
233
|
+
sentAtMs: number;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export interface TelegramBusSelectedMenuDeliveryObservation {
|
|
237
|
+
operationId: string;
|
|
238
|
+
recipient: TelegramBusSelectedMenuDelivery["recipient"];
|
|
239
|
+
registrationGeneration: string;
|
|
240
|
+
effect: TelegramBusSelectedMenuTextEffect["kind"];
|
|
241
|
+
messageId: number;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Closed wire projection of prepared recipient source; independent of journal implementation and never a removal grant. */
|
|
245
|
+
export interface TelegramBusPreparedCommandSource {
|
|
246
|
+
journalBindingKey: string;
|
|
247
|
+
updateId: number;
|
|
248
|
+
sourceSha256: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Pure live-rebind recipient/operation identity shared by prepare/apply/settle envelopes. */
|
|
252
|
+
function parseLiveRebindRecipient(value: Record<string, unknown>):
|
|
253
|
+
| {
|
|
254
|
+
recipientInstanceId: string;
|
|
255
|
+
recipientRegistrationGeneration: string;
|
|
256
|
+
recipientSessionId: string;
|
|
257
|
+
recipientBindingKey: string;
|
|
258
|
+
operationId: string;
|
|
259
|
+
}
|
|
260
|
+
| undefined {
|
|
261
|
+
const {
|
|
262
|
+
recipientInstanceId,
|
|
263
|
+
recipientRegistrationGeneration,
|
|
264
|
+
recipientSessionId,
|
|
265
|
+
recipientBindingKey,
|
|
266
|
+
operationId,
|
|
267
|
+
} = value;
|
|
268
|
+
return typeof recipientInstanceId === "string" &&
|
|
269
|
+
recipientInstanceId &&
|
|
270
|
+
typeof recipientRegistrationGeneration === "string" &&
|
|
271
|
+
recipientRegistrationGeneration &&
|
|
272
|
+
typeof recipientSessionId === "string" &&
|
|
273
|
+
recipientSessionId &&
|
|
274
|
+
typeof recipientBindingKey === "string" &&
|
|
275
|
+
recipientBindingKey &&
|
|
276
|
+
typeof operationId === "string" &&
|
|
277
|
+
operationId.length > 0 &&
|
|
278
|
+
operationId.length <= 128
|
|
279
|
+
? {
|
|
280
|
+
recipientInstanceId,
|
|
281
|
+
recipientRegistrationGeneration,
|
|
282
|
+
recipientSessionId,
|
|
283
|
+
recipientBindingKey,
|
|
284
|
+
operationId,
|
|
285
|
+
}
|
|
286
|
+
: undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Non-empty, duplicate-free, non-negative safe-integer Telegram update ids. */
|
|
290
|
+
function isUniqueSourceUpdateIds(value: unknown): value is number[] {
|
|
291
|
+
return (
|
|
292
|
+
Array.isArray(value) &&
|
|
293
|
+
value.length > 0 &&
|
|
294
|
+
value.every((id) => Number.isSafeInteger(id) && (id as number) >= 0) &&
|
|
295
|
+
new Set(value).size === value.length
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function parsePreparedCommandSource(
|
|
300
|
+
value: unknown,
|
|
301
|
+
): TelegramBusPreparedCommandSource | undefined {
|
|
302
|
+
if (
|
|
303
|
+
!isRecord(value) ||
|
|
304
|
+
!hasOnlyWireKeys(value, [
|
|
305
|
+
"journalBindingKey",
|
|
306
|
+
"updateId",
|
|
307
|
+
"sourceSha256",
|
|
308
|
+
]) ||
|
|
309
|
+
typeof value.journalBindingKey !== "string" ||
|
|
310
|
+
!value.journalBindingKey ||
|
|
311
|
+
!Number.isSafeInteger(value.updateId) ||
|
|
312
|
+
(value.updateId as number) < 0 ||
|
|
313
|
+
typeof value.sourceSha256 !== "string" ||
|
|
314
|
+
!/^[a-f0-9]{64}$/u.test(value.sourceSha256)
|
|
315
|
+
)
|
|
316
|
+
return undefined;
|
|
317
|
+
return {
|
|
318
|
+
journalBindingKey: value.journalBindingKey,
|
|
319
|
+
updateId: value.updateId as number,
|
|
320
|
+
sourceSha256: value.sourceSha256,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export interface TelegramBusLiveRebindSaveObservation {
|
|
325
|
+
operationId: string;
|
|
326
|
+
recipient: {
|
|
327
|
+
instanceId: string;
|
|
328
|
+
sessionId: string;
|
|
329
|
+
generation: string;
|
|
330
|
+
bindingKey: string;
|
|
331
|
+
};
|
|
332
|
+
sourceUpdateIds: number[];
|
|
333
|
+
selectedCommand?: TelegramBusSelectedCommandInput;
|
|
334
|
+
preparedSource?: TelegramBusPreparedCommandSource;
|
|
335
|
+
status: "saved";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Read-only warm observation; semantic completion and exact source ACK are distinct from detached delivery. */
|
|
339
|
+
export interface TelegramBusLiveRebindCommandObservation extends Omit<
|
|
340
|
+
TelegramBusLiveRebindSaveObservation,
|
|
341
|
+
"status" | "preparedSource" | "selectedCommand"
|
|
342
|
+
> {
|
|
343
|
+
status: "command-observed";
|
|
344
|
+
selectedCommand: TelegramBusSelectedCommandInput;
|
|
345
|
+
preparedSource: TelegramBusPreparedCommandSource;
|
|
346
|
+
command: "completed" | "unknown" | "not-issued";
|
|
347
|
+
sourceAck?: TelegramBusPreparedCommandSource;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export interface TelegramBusLiveRebindWorkState {
|
|
351
|
+
sessionBusy: boolean;
|
|
352
|
+
targetWork: boolean;
|
|
353
|
+
deliveryPending: boolean;
|
|
354
|
+
unknown: boolean;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export interface TelegramBusLiveRebindWorkObservation extends Omit<
|
|
358
|
+
TelegramBusLiveRebindSaveObservation,
|
|
359
|
+
"status"
|
|
360
|
+
> {
|
|
361
|
+
status: "observed";
|
|
362
|
+
oldTarget: TelegramTarget & { threadId: number };
|
|
363
|
+
work: TelegramBusLiveRebindWorkState;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export interface TelegramBusLiveRebindSettlementObservation extends Omit<
|
|
367
|
+
TelegramBusLiveRebindSaveObservation,
|
|
368
|
+
"status"
|
|
369
|
+
> {
|
|
370
|
+
action: "release" | "discard";
|
|
371
|
+
status: "released" | "discarded" | "protected" | "unknown";
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export interface TelegramBusLiveRebindApplyObservation extends Omit<
|
|
375
|
+
TelegramBusLiveRebindSaveObservation,
|
|
376
|
+
"status"
|
|
377
|
+
> {
|
|
378
|
+
status: "saved" | "applied";
|
|
379
|
+
target: TelegramTarget & { threadId: number };
|
|
380
|
+
slot: string;
|
|
381
|
+
}
|
|
253
382
|
|
|
254
383
|
export interface TelegramBusWorkspaceRestoreObservation {
|
|
255
384
|
operationId: string;
|
|
256
|
-
recipient: {
|
|
385
|
+
recipient: {
|
|
386
|
+
kind: "follower";
|
|
387
|
+
instanceId: string;
|
|
388
|
+
sessionId: string;
|
|
389
|
+
generation: string;
|
|
390
|
+
};
|
|
257
391
|
target: TelegramTarget & { threadId: number };
|
|
258
392
|
slot: string;
|
|
259
393
|
ready: boolean;
|
|
@@ -284,8 +418,7 @@ export function createTelegramBusProtocolIdentity(input: {
|
|
|
284
418
|
capabilities.length > 32 ||
|
|
285
419
|
capabilities.some(
|
|
286
420
|
(capability) =>
|
|
287
|
-
capability.length > 128 ||
|
|
288
|
-
!/^[a-z0-9][a-z0-9._-]*$/u.test(capability),
|
|
421
|
+
capability.length > 128 || !/^[a-z0-9][a-z0-9._-]*$/u.test(capability),
|
|
289
422
|
)
|
|
290
423
|
) {
|
|
291
424
|
throw new Error("Telegram bus capabilities must be canonical identifiers.");
|
|
@@ -320,18 +453,42 @@ export function hasTelegramBusCapability(
|
|
|
320
453
|
}
|
|
321
454
|
|
|
322
455
|
export function getTelegramInputCustodyPeerReadiness(
|
|
323
|
-
followers: readonly Pick<
|
|
324
|
-
|
|
456
|
+
followers: readonly Pick<
|
|
457
|
+
TelegramBusFollowerView,
|
|
458
|
+
"registrationGeneration" | "protocol"
|
|
459
|
+
>[],
|
|
325
460
|
): ("ready" | "legacy" | "unknown")[] {
|
|
326
|
-
return followers.map(follower => {
|
|
327
|
-
if (!follower.registrationGeneration || !follower.protocol)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
461
|
+
return followers.map((follower) => {
|
|
462
|
+
if (!follower.registrationGeneration || !follower.protocol)
|
|
463
|
+
return "unknown";
|
|
464
|
+
return hasTelegramBusCapability(
|
|
465
|
+
follower.protocol,
|
|
466
|
+
TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
|
|
467
|
+
) &&
|
|
468
|
+
hasTelegramBusCapability(
|
|
469
|
+
follower.protocol,
|
|
470
|
+
TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE,
|
|
471
|
+
)
|
|
472
|
+
? "ready"
|
|
473
|
+
: "legacy";
|
|
332
474
|
});
|
|
333
475
|
}
|
|
334
476
|
|
|
477
|
+
/** Both peers advertise every required capability over a compatible protocol; reads only the two identities. */
|
|
478
|
+
export function hasTelegramBusSharedCapabilities(
|
|
479
|
+
local: TelegramBusProtocolIdentity,
|
|
480
|
+
remote: TelegramBusProtocolIdentity | undefined,
|
|
481
|
+
required: readonly string[],
|
|
482
|
+
): boolean {
|
|
483
|
+
return (
|
|
484
|
+
required.every(
|
|
485
|
+
(capability) =>
|
|
486
|
+
hasTelegramBusCapability(local, capability) &&
|
|
487
|
+
hasTelegramBusCapability(remote, capability),
|
|
488
|
+
) && getTelegramBusProtocolCompatibility({ local, remote }).compatible
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
335
492
|
export function getTelegramBusProtocolCompatibility(input: {
|
|
336
493
|
local: TelegramBusProtocolIdentity;
|
|
337
494
|
remote?: TelegramBusProtocolIdentity;
|
|
@@ -372,7 +529,12 @@ export function getTelegramBusSocketPath(
|
|
|
372
529
|
profileName?: string,
|
|
373
530
|
layout?: TelegramBusEndpointLayout,
|
|
374
531
|
): string {
|
|
375
|
-
return getTelegramBusLeaderEndpoint({
|
|
532
|
+
return getTelegramBusLeaderEndpoint({
|
|
533
|
+
agentDir,
|
|
534
|
+
platform,
|
|
535
|
+
profileName,
|
|
536
|
+
layout,
|
|
537
|
+
});
|
|
376
538
|
}
|
|
377
539
|
|
|
378
540
|
export function getTelegramBusFollowerSocketPath(
|
|
@@ -413,6 +575,26 @@ export interface TelegramBusFollowerView extends TelegramBusInstanceRegistration
|
|
|
413
575
|
lastHeartbeatMs: number;
|
|
414
576
|
}
|
|
415
577
|
|
|
578
|
+
/** Whether a live follower is still exactly the captured registration: identity, process, session, endpoint and negotiated protocol. */
|
|
579
|
+
export function isSameTelegramBusFollowerRegistration(
|
|
580
|
+
live: TelegramBusInstanceRegistration,
|
|
581
|
+
captured: TelegramBusInstanceRegistration,
|
|
582
|
+
): boolean {
|
|
583
|
+
return (
|
|
584
|
+
live.instanceId === captured.instanceId &&
|
|
585
|
+
live.profileKey === captured.profileKey &&
|
|
586
|
+
live.sessionId === captured.sessionId &&
|
|
587
|
+
live.registrationGeneration === captured.registrationGeneration &&
|
|
588
|
+
live.pid === captured.pid &&
|
|
589
|
+
live.processBirthId === captured.processBirthId &&
|
|
590
|
+
live.sessionGeneration === captured.sessionGeneration &&
|
|
591
|
+
live.cwd === captured.cwd &&
|
|
592
|
+
live.slot === captured.slot &&
|
|
593
|
+
live.busSocketPath === captured.busSocketPath &&
|
|
594
|
+
isDeepStrictEqual(live.protocol, captured.protocol)
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
416
598
|
export function getTelegramFollowerTargetOwnership(input: {
|
|
417
599
|
target: TelegramTarget;
|
|
418
600
|
followers: readonly TelegramBusFollowerView[];
|
|
@@ -424,12 +606,14 @@ export function getTelegramFollowerTargetOwnership(input: {
|
|
|
424
606
|
target: TelegramTarget;
|
|
425
607
|
}[];
|
|
426
608
|
currentInstanceId?: string;
|
|
427
|
-
}):
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
609
|
+
}):
|
|
610
|
+
| {
|
|
611
|
+
instanceId: string;
|
|
612
|
+
ownerGeneration: string;
|
|
613
|
+
recipientBindingKey: string;
|
|
614
|
+
protocolIdentity: TelegramBusProtocolIdentity;
|
|
615
|
+
}
|
|
616
|
+
| undefined {
|
|
433
617
|
const liveFollower = input.followers.find((follower) => {
|
|
434
618
|
return (
|
|
435
619
|
follower.target?.chatId === input.target.chatId &&
|
|
@@ -459,43 +643,32 @@ const TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD = "__piTelegramAggregateDelivery";
|
|
|
459
643
|
const TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD =
|
|
460
644
|
"__piTelegramCrossTargetDelivery";
|
|
461
645
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
return {
|
|
466
|
-
...body,
|
|
467
|
-
[TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD]: true,
|
|
468
|
-
};
|
|
469
|
-
}
|
|
646
|
+
type TelegramBusDeliveryMarker =
|
|
647
|
+
| typeof TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD
|
|
648
|
+
| typeof TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD;
|
|
470
649
|
|
|
471
|
-
function
|
|
650
|
+
function hasTelegramBusDeliveryMarker(
|
|
651
|
+
body: unknown,
|
|
652
|
+
marker: TelegramBusDeliveryMarker,
|
|
653
|
+
): boolean {
|
|
472
654
|
return Boolean(
|
|
473
655
|
body &&
|
|
474
656
|
typeof body === "object" &&
|
|
475
657
|
!Array.isArray(body) &&
|
|
476
|
-
(body as Record<string, unknown>)[
|
|
477
|
-
true,
|
|
658
|
+
(body as Record<string, unknown>)[marker] === true,
|
|
478
659
|
);
|
|
479
660
|
}
|
|
480
661
|
|
|
481
|
-
export function
|
|
662
|
+
export function markTelegramBusAggregateDelivery<
|
|
482
663
|
T extends Record<string, unknown>,
|
|
483
664
|
>(body: T): T {
|
|
484
|
-
return {
|
|
485
|
-
...body,
|
|
486
|
-
[TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD]: true,
|
|
487
|
-
};
|
|
665
|
+
return { ...body, [TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD]: true };
|
|
488
666
|
}
|
|
489
667
|
|
|
490
|
-
function
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
!Array.isArray(body) &&
|
|
495
|
-
(body as Record<string, unknown>)[
|
|
496
|
-
TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD
|
|
497
|
-
] === true,
|
|
498
|
-
);
|
|
668
|
+
export function markTelegramBusCrossTargetDelivery<
|
|
669
|
+
T extends Record<string, unknown>,
|
|
670
|
+
>(body: T): T {
|
|
671
|
+
return { ...body, [TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD]: true };
|
|
499
672
|
}
|
|
500
673
|
|
|
501
674
|
export function stripTelegramBusApiMetadata<T extends Record<string, unknown>>(
|
|
@@ -567,8 +740,7 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
567
740
|
typeof threadId === "number" ? threadId : Number(threadId);
|
|
568
741
|
return (
|
|
569
742
|
Number.isInteger(parsedThreadId) &&
|
|
570
|
-
(target.threadId === undefined ||
|
|
571
|
-
!matchesId(threadId, target.threadId))
|
|
743
|
+
(target.threadId === undefined || !matchesId(threadId, target.threadId))
|
|
572
744
|
);
|
|
573
745
|
};
|
|
574
746
|
const isTargetMessageScoped = (body: unknown): boolean => {
|
|
@@ -612,14 +784,20 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
612
784
|
return isTargetChatScoped(input.args[1]);
|
|
613
785
|
if (
|
|
614
786
|
apiMethod === "sendMessage" &&
|
|
615
|
-
|
|
787
|
+
hasTelegramBusDeliveryMarker(
|
|
788
|
+
input.args[1],
|
|
789
|
+
TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD,
|
|
790
|
+
)
|
|
616
791
|
) {
|
|
617
792
|
const body = input.args[1] as Record<string, unknown>;
|
|
618
793
|
return body.message_thread_id === undefined && isTargetChatScoped(body);
|
|
619
794
|
}
|
|
620
795
|
if (
|
|
621
796
|
(apiMethod === "sendMessage" || apiMethod === "sendRichMessage") &&
|
|
622
|
-
|
|
797
|
+
hasTelegramBusDeliveryMarker(
|
|
798
|
+
input.args[1],
|
|
799
|
+
TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD,
|
|
800
|
+
)
|
|
623
801
|
) {
|
|
624
802
|
return isDifferentTargetScoped(input.args[1]);
|
|
625
803
|
}
|
|
@@ -744,8 +922,10 @@ export function createTelegramBusFollowerDeliveryIdentity(input: {
|
|
|
744
922
|
!Number.isSafeInteger(input.sourceUpdateId) ||
|
|
745
923
|
input.sourceUpdateId < 0 ||
|
|
746
924
|
(input.sourceClaim !== undefined &&
|
|
747
|
-
(!input.sourceClaim.acquisitionId ||
|
|
748
|
-
|
|
925
|
+
(!input.sourceClaim.acquisitionId ||
|
|
926
|
+
input.sourceClaim.acquisitionId.length > 256 ||
|
|
927
|
+
!input.sourceClaim.handoffId ||
|
|
928
|
+
input.sourceClaim.handoffId.length > 256))
|
|
749
929
|
) {
|
|
750
930
|
throw new Error("Telegram follower delivery identity is incomplete.");
|
|
751
931
|
}
|
|
@@ -763,7 +943,9 @@ export function createTelegramBusFollowerDeliveryIdentity(input: {
|
|
|
763
943
|
deliveryId: `telegram-follower-v1-${deliveryId}`,
|
|
764
944
|
sourceUpdateId: input.sourceUpdateId,
|
|
765
945
|
recipientBindingKey: input.recipientBindingKey,
|
|
766
|
-
...(input.sourceRecoveryKey
|
|
946
|
+
...(input.sourceRecoveryKey
|
|
947
|
+
? { sourceRecoveryKey: input.sourceRecoveryKey }
|
|
948
|
+
: {}),
|
|
767
949
|
...(input.sourceClaim ? { sourceClaim: { ...input.sourceClaim } } : {}),
|
|
768
950
|
};
|
|
769
951
|
}
|
|
@@ -772,26 +954,46 @@ export function canUseTelegramBusInputCustodyReference(input: {
|
|
|
772
954
|
local?: TelegramBusProtocolIdentity;
|
|
773
955
|
remote?: TelegramBusProtocolIdentity;
|
|
774
956
|
}): boolean {
|
|
775
|
-
return
|
|
776
|
-
hasTelegramBusCapability(
|
|
957
|
+
return (
|
|
958
|
+
hasTelegramBusCapability(
|
|
959
|
+
input.local,
|
|
960
|
+
TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE,
|
|
961
|
+
) &&
|
|
962
|
+
hasTelegramBusCapability(
|
|
963
|
+
input.remote,
|
|
964
|
+
TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE,
|
|
965
|
+
)
|
|
966
|
+
);
|
|
777
967
|
}
|
|
778
968
|
|
|
779
969
|
export function createTelegramBusFollowerSourceReferenceDeliveryIdentity(input: {
|
|
780
|
-
kind:
|
|
781
|
-
|
|
970
|
+
kind:
|
|
971
|
+
| "leader.forwardCallback"
|
|
972
|
+
| "leader.forwardReaction"
|
|
973
|
+
| "leader.forwardMessage"
|
|
974
|
+
| "leader.forwardEditedMessage"
|
|
975
|
+
| "leader.wakeInputCustody";
|
|
782
976
|
recipientBindingKey: string;
|
|
783
977
|
sourceRecoveryKey: string;
|
|
784
|
-
source: {
|
|
978
|
+
source: {
|
|
979
|
+
updateId: number;
|
|
980
|
+
owner: { acquisitionId: string; handoffId?: string };
|
|
981
|
+
};
|
|
785
982
|
}): TelegramBusFollowerDeliveryIdentity {
|
|
786
|
-
if (!input.sourceRecoveryKey || !input.source.owner.handoffId)
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
983
|
+
if (!input.sourceRecoveryKey || !input.source.owner.handoffId)
|
|
984
|
+
throw new Error(
|
|
985
|
+
"Telegram follower source-reference delivery requires an accepted handoff.",
|
|
986
|
+
);
|
|
987
|
+
return createTelegramBusFollowerDeliveryIdentity({
|
|
988
|
+
kind: input.kind,
|
|
790
989
|
recipientBindingKey: input.recipientBindingKey,
|
|
791
990
|
sourceUpdateId: input.source.updateId,
|
|
792
991
|
sourceRecoveryKey: input.sourceRecoveryKey,
|
|
793
|
-
sourceClaim: {
|
|
794
|
-
|
|
992
|
+
sourceClaim: {
|
|
993
|
+
acquisitionId: input.source.owner.acquisitionId,
|
|
994
|
+
handoffId: input.source.owner.handoffId,
|
|
995
|
+
},
|
|
996
|
+
});
|
|
795
997
|
}
|
|
796
998
|
|
|
797
999
|
export type TelegramBusEnvelope = (
|
|
@@ -897,7 +1099,11 @@ export type TelegramBusEnvelope = (
|
|
|
897
1099
|
recipientRegistrationGeneration: string;
|
|
898
1100
|
recipientBindingKey: string;
|
|
899
1101
|
sourceRecoveryKey: string;
|
|
900
|
-
source: {
|
|
1102
|
+
source: {
|
|
1103
|
+
journalBindingKey: string;
|
|
1104
|
+
tokenSha256: string;
|
|
1105
|
+
updateId: number;
|
|
1106
|
+
};
|
|
901
1107
|
handoffId: string;
|
|
902
1108
|
sentAtMs: number;
|
|
903
1109
|
}
|
|
@@ -909,6 +1115,47 @@ export type TelegramBusEnvelope = (
|
|
|
909
1115
|
delivery: TelegramBusFollowerDeliveryIdentity;
|
|
910
1116
|
sentAtMs: number;
|
|
911
1117
|
}
|
|
1118
|
+
| {
|
|
1119
|
+
kind: "leader.prepareLiveRebind";
|
|
1120
|
+
requestId: string;
|
|
1121
|
+
recipientInstanceId: string;
|
|
1122
|
+
recipientRegistrationGeneration: string;
|
|
1123
|
+
recipientSessionId: string;
|
|
1124
|
+
recipientBindingKey: string;
|
|
1125
|
+
operationId: string;
|
|
1126
|
+
updates: ({ update_id: number } & Record<string, unknown>)[];
|
|
1127
|
+
selectedCommand?: TelegramBusSelectedCommandInput;
|
|
1128
|
+
sentAtMs: number;
|
|
1129
|
+
}
|
|
1130
|
+
| {
|
|
1131
|
+
kind: "leader.applyLiveRebind";
|
|
1132
|
+
requestId: string;
|
|
1133
|
+
recipientInstanceId: string;
|
|
1134
|
+
recipientRegistrationGeneration: string;
|
|
1135
|
+
recipientSessionId: string;
|
|
1136
|
+
recipientBindingKey: string;
|
|
1137
|
+
operationId: string;
|
|
1138
|
+
sourceUpdateIds: number[];
|
|
1139
|
+
mode: "apply" | "inspect";
|
|
1140
|
+
preparedSource?: TelegramBusPreparedCommandSource;
|
|
1141
|
+
selectedCommand?: TelegramBusSelectedCommandInput;
|
|
1142
|
+
sentAtMs: number;
|
|
1143
|
+
}
|
|
1144
|
+
| {
|
|
1145
|
+
kind: "leader.settleLiveRebind";
|
|
1146
|
+
requestId: string;
|
|
1147
|
+
recipientInstanceId: string;
|
|
1148
|
+
recipientRegistrationGeneration: string;
|
|
1149
|
+
recipientSessionId: string;
|
|
1150
|
+
recipientBindingKey: string;
|
|
1151
|
+
operationId: string;
|
|
1152
|
+
sourceUpdateIds: number[];
|
|
1153
|
+
mode: "release" | "discard" | "observe" | "observe-command";
|
|
1154
|
+
oldTarget?: TelegramTarget & { threadId: number };
|
|
1155
|
+
preparedSource?: TelegramBusPreparedCommandSource;
|
|
1156
|
+
selectedCommand?: TelegramBusSelectedCommandInput;
|
|
1157
|
+
sentAtMs: number;
|
|
1158
|
+
}
|
|
912
1159
|
| {
|
|
913
1160
|
kind: "leader.workspaceRestore";
|
|
914
1161
|
requestId: string;
|
|
@@ -965,6 +1212,7 @@ export type TelegramBusEnvelope = (
|
|
|
965
1212
|
message: TelegramBusAgentMessage;
|
|
966
1213
|
sentAtMs: number;
|
|
967
1214
|
}
|
|
1215
|
+
| TelegramBusSelectedMenuDelivery
|
|
968
1216
|
| {
|
|
969
1217
|
kind: "follower.callApi";
|
|
970
1218
|
requestId: string;
|
|
@@ -996,10 +1244,25 @@ export type TelegramBusEnvelope = (
|
|
|
996
1244
|
}
|
|
997
1245
|
) & { auth?: string };
|
|
998
1246
|
|
|
1247
|
+
/** Follower-staged queue handoff offer before the follower adds its own request and registration framing. */
|
|
1248
|
+
export type TelegramBusFollowerQueueHandoffOffer = Omit<
|
|
1249
|
+
Extract<TelegramBusEnvelope, { kind: "follower.offerQueueHandoff" }>,
|
|
1250
|
+
| "kind"
|
|
1251
|
+
| "requestId"
|
|
1252
|
+
| "instanceId"
|
|
1253
|
+
| "registrationGeneration"
|
|
1254
|
+
| "sentAtMs"
|
|
1255
|
+
| "auth"
|
|
1256
|
+
>;
|
|
1257
|
+
|
|
1258
|
+
/** Leader-routed queue handoff offer as donors and leaders exchange it before envelope framing. */
|
|
1259
|
+
export type TelegramBusLeaderQueueHandoffOffer = Omit<
|
|
1260
|
+
Extract<TelegramBusEnvelope, { kind: "leader.offerQueueHandoff" }>,
|
|
1261
|
+
"kind"
|
|
1262
|
+
>;
|
|
1263
|
+
|
|
999
1264
|
type TelegramBusEnvelopeTrafficClass =
|
|
1000
|
-
| "
|
|
1001
|
-
| "generation-fenced"
|
|
1002
|
-
| "response";
|
|
1265
|
+
"bootstrap" | "generation-fenced" | "response";
|
|
1003
1266
|
|
|
1004
1267
|
export function getTelegramBusEnvelopeTrafficClass(
|
|
1005
1268
|
envelope: TelegramBusEnvelope,
|
|
@@ -1007,7 +1270,8 @@ export function getTelegramBusEnvelopeTrafficClass(
|
|
|
1007
1270
|
if (
|
|
1008
1271
|
envelope.kind === "follower.register" ||
|
|
1009
1272
|
envelope.kind === "follower.restoreWorkspace"
|
|
1010
|
-
)
|
|
1273
|
+
)
|
|
1274
|
+
return "bootstrap";
|
|
1011
1275
|
if (envelope.kind === "bus.ack") return "response";
|
|
1012
1276
|
return "generation-fenced";
|
|
1013
1277
|
}
|
|
@@ -1057,18 +1321,25 @@ export function parseTelegramBusEnvelope(
|
|
|
1057
1321
|
envelope = parseRegisterEnvelope(value, requestId, kind);
|
|
1058
1322
|
break;
|
|
1059
1323
|
case "follower.heartbeat":
|
|
1060
|
-
envelope = parseHeartbeatEnvelope(value, requestId);
|
|
1061
|
-
break;
|
|
1062
1324
|
case "follower.disconnect":
|
|
1063
|
-
envelope =
|
|
1325
|
+
envelope = parseFollowerPresenceEnvelope(value, requestId, kind);
|
|
1064
1326
|
break;
|
|
1065
1327
|
case "follower.setThreadDisplayMode":
|
|
1066
|
-
if (
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1328
|
+
if (
|
|
1329
|
+
typeof value.instanceId === "string" &&
|
|
1330
|
+
typeof value.registrationGeneration === "string" &&
|
|
1331
|
+
(value.mode === "letters" ||
|
|
1332
|
+
value.mode === "names" ||
|
|
1333
|
+
value.mode === "directory-snake" ||
|
|
1334
|
+
value.mode === "directory-title")
|
|
1335
|
+
) {
|
|
1336
|
+
envelope = {
|
|
1337
|
+
kind,
|
|
1338
|
+
requestId,
|
|
1339
|
+
instanceId: value.instanceId,
|
|
1340
|
+
registrationGeneration: value.registrationGeneration,
|
|
1341
|
+
mode: value.mode,
|
|
1342
|
+
};
|
|
1072
1343
|
}
|
|
1073
1344
|
break;
|
|
1074
1345
|
case "follower.renameThread":
|
|
@@ -1076,10 +1347,12 @@ export function parseTelegramBusEnvelope(
|
|
|
1076
1347
|
break;
|
|
1077
1348
|
case "follower.resetThreadName": {
|
|
1078
1349
|
const target = parseTarget(value.target);
|
|
1079
|
-
if (
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1350
|
+
if (
|
|
1351
|
+
typeof value.instanceId === "string" &&
|
|
1352
|
+
typeof value.registrationGeneration === "string" &&
|
|
1353
|
+
typeof value.sentAtMs === "number" &&
|
|
1354
|
+
target?.threadId !== undefined
|
|
1355
|
+
) {
|
|
1083
1356
|
envelope = {
|
|
1084
1357
|
kind,
|
|
1085
1358
|
requestId,
|
|
@@ -1094,9 +1367,12 @@ export function parseTelegramBusEnvelope(
|
|
|
1094
1367
|
case "follower.publishSessionReplacement":
|
|
1095
1368
|
case "follower.settleSessionReplacement": {
|
|
1096
1369
|
const intent = normalizeTelegramSessionReplacementIntent(value.intent);
|
|
1097
|
-
if (
|
|
1098
|
-
|
|
1099
|
-
|
|
1370
|
+
if (
|
|
1371
|
+
typeof value.instanceId === "string" &&
|
|
1372
|
+
typeof value.registrationGeneration === "string" &&
|
|
1373
|
+
typeof value.sentAtMs === "number" &&
|
|
1374
|
+
intent
|
|
1375
|
+
) {
|
|
1100
1376
|
envelope = {
|
|
1101
1377
|
kind,
|
|
1102
1378
|
requestId,
|
|
@@ -1134,15 +1410,147 @@ export function parseTelegramBusEnvelope(
|
|
|
1134
1410
|
"leader.forwardEditedMessage",
|
|
1135
1411
|
);
|
|
1136
1412
|
break;
|
|
1413
|
+
case "leader.prepareLiveRebind": {
|
|
1414
|
+
const selectedCommand = parseSelectedCommandInput(value.selectedCommand);
|
|
1415
|
+
if (
|
|
1416
|
+
!hasOnlyWireKeys(value, [
|
|
1417
|
+
"kind",
|
|
1418
|
+
"requestId",
|
|
1419
|
+
"auth",
|
|
1420
|
+
"recipientInstanceId",
|
|
1421
|
+
"recipientRegistrationGeneration",
|
|
1422
|
+
"recipientSessionId",
|
|
1423
|
+
"recipientBindingKey",
|
|
1424
|
+
"operationId",
|
|
1425
|
+
"updates",
|
|
1426
|
+
"selectedCommand",
|
|
1427
|
+
"sentAtMs",
|
|
1428
|
+
]) ||
|
|
1429
|
+
(Object.hasOwn(value, "selectedCommand") &&
|
|
1430
|
+
(!selectedCommand ||
|
|
1431
|
+
!Array.isArray(value.updates) ||
|
|
1432
|
+
value.updates.length !== 1))
|
|
1433
|
+
)
|
|
1434
|
+
break;
|
|
1435
|
+
const recipient = parseLiveRebindRecipient(value);
|
|
1436
|
+
if (
|
|
1437
|
+
recipient &&
|
|
1438
|
+
Array.isArray(value.updates) &&
|
|
1439
|
+
value.updates.every(isRecord) &&
|
|
1440
|
+
isUniqueSourceUpdateIds(
|
|
1441
|
+
value.updates.map((update) => update.update_id),
|
|
1442
|
+
) &&
|
|
1443
|
+
typeof value.sentAtMs === "number" &&
|
|
1444
|
+
Number.isFinite(value.sentAtMs)
|
|
1445
|
+
) {
|
|
1446
|
+
envelope = {
|
|
1447
|
+
kind: "leader.prepareLiveRebind",
|
|
1448
|
+
requestId,
|
|
1449
|
+
...recipient,
|
|
1450
|
+
updates: value.updates as ({ update_id: number } & Record<
|
|
1451
|
+
string,
|
|
1452
|
+
unknown
|
|
1453
|
+
>)[],
|
|
1454
|
+
sentAtMs: value.sentAtMs,
|
|
1455
|
+
...(selectedCommand ? { selectedCommand } : {}),
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
case "leader.applyLiveRebind":
|
|
1461
|
+
case "leader.settleLiveRebind": {
|
|
1462
|
+
const selectedCommand = parseSelectedCommandInput(value.selectedCommand),
|
|
1463
|
+
preparedSource = parsePreparedCommandSource(value.preparedSource);
|
|
1464
|
+
if (
|
|
1465
|
+
!hasOnlyWireKeys(value, [
|
|
1466
|
+
"kind",
|
|
1467
|
+
"requestId",
|
|
1468
|
+
"auth",
|
|
1469
|
+
"recipientInstanceId",
|
|
1470
|
+
"recipientRegistrationGeneration",
|
|
1471
|
+
"recipientSessionId",
|
|
1472
|
+
"recipientBindingKey",
|
|
1473
|
+
"operationId",
|
|
1474
|
+
"sourceUpdateIds",
|
|
1475
|
+
"mode",
|
|
1476
|
+
"oldTarget",
|
|
1477
|
+
"selectedCommand",
|
|
1478
|
+
"preparedSource",
|
|
1479
|
+
"sentAtMs",
|
|
1480
|
+
]) ||
|
|
1481
|
+
(Object.hasOwn(value, "preparedSource") && !selectedCommand) ||
|
|
1482
|
+
(value.mode === "observe-command" &&
|
|
1483
|
+
(!selectedCommand || Object.hasOwn(value, "oldTarget")))
|
|
1484
|
+
)
|
|
1485
|
+
break;
|
|
1486
|
+
if (
|
|
1487
|
+
Object.hasOwn(value, "selectedCommand") &&
|
|
1488
|
+
(!selectedCommand ||
|
|
1489
|
+
!Array.isArray(value.sourceUpdateIds) ||
|
|
1490
|
+
value.sourceUpdateIds.length !== 1 ||
|
|
1491
|
+
!preparedSource ||
|
|
1492
|
+
preparedSource.updateId !== value.sourceUpdateIds[0] ||
|
|
1493
|
+
preparedSource.journalBindingKey !== value.recipientBindingKey)
|
|
1494
|
+
)
|
|
1495
|
+
break;
|
|
1496
|
+
const recipient = parseLiveRebindRecipient(value);
|
|
1497
|
+
if (
|
|
1498
|
+
recipient &&
|
|
1499
|
+
(value.kind === "leader.applyLiveRebind"
|
|
1500
|
+
? value.mode === "apply" || value.mode === "inspect"
|
|
1501
|
+
: value.mode === "release" ||
|
|
1502
|
+
value.mode === "discard" ||
|
|
1503
|
+
value.mode === "observe-command" ||
|
|
1504
|
+
(value.mode === "observe" &&
|
|
1505
|
+
!!parseThreadTarget(value.oldTarget))) &&
|
|
1506
|
+
isUniqueSourceUpdateIds(value.sourceUpdateIds) &&
|
|
1507
|
+
typeof value.sentAtMs === "number" &&
|
|
1508
|
+
Number.isFinite(value.sentAtMs)
|
|
1509
|
+
) {
|
|
1510
|
+
envelope = {
|
|
1511
|
+
kind: value.kind,
|
|
1512
|
+
requestId,
|
|
1513
|
+
...recipient,
|
|
1514
|
+
sourceUpdateIds: value.sourceUpdateIds,
|
|
1515
|
+
mode: value.mode as
|
|
1516
|
+
| "apply"
|
|
1517
|
+
| "inspect"
|
|
1518
|
+
| "release"
|
|
1519
|
+
| "discard"
|
|
1520
|
+
| "observe"
|
|
1521
|
+
| "observe-command",
|
|
1522
|
+
sentAtMs: value.sentAtMs,
|
|
1523
|
+
...(value.mode === "observe"
|
|
1524
|
+
? { oldTarget: parseThreadTarget(value.oldTarget)! }
|
|
1525
|
+
: {}),
|
|
1526
|
+
...(selectedCommand ? { selectedCommand, preparedSource } : {}),
|
|
1527
|
+
} as TelegramBusEnvelope;
|
|
1528
|
+
}
|
|
1529
|
+
break;
|
|
1530
|
+
}
|
|
1137
1531
|
case "leader.workspaceRestore":
|
|
1138
|
-
if (
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1532
|
+
if (
|
|
1533
|
+
typeof value.recipientInstanceId === "string" &&
|
|
1534
|
+
value.recipientInstanceId &&
|
|
1535
|
+
typeof value.recipientRegistrationGeneration === "string" &&
|
|
1536
|
+
value.recipientRegistrationGeneration &&
|
|
1537
|
+
typeof value.operationId === "string" &&
|
|
1538
|
+
value.operationId.length > 0 &&
|
|
1539
|
+
value.operationId.length <= 128 &&
|
|
1540
|
+
(value.mode === "apply" || value.mode === "inspect") &&
|
|
1541
|
+
typeof value.sentAtMs === "number" &&
|
|
1542
|
+
Number.isFinite(value.sentAtMs)
|
|
1543
|
+
) {
|
|
1544
|
+
envelope = {
|
|
1545
|
+
kind: "leader.workspaceRestore",
|
|
1546
|
+
requestId,
|
|
1547
|
+
recipientInstanceId: value.recipientInstanceId,
|
|
1548
|
+
recipientRegistrationGeneration:
|
|
1549
|
+
value.recipientRegistrationGeneration,
|
|
1550
|
+
operationId: value.operationId,
|
|
1551
|
+
mode: value.mode,
|
|
1552
|
+
sentAtMs: value.sentAtMs,
|
|
1553
|
+
};
|
|
1146
1554
|
}
|
|
1147
1555
|
break;
|
|
1148
1556
|
case "leader.offerQueueHandoff":
|
|
@@ -1165,6 +1573,9 @@ export function parseTelegramBusEnvelope(
|
|
|
1165
1573
|
case "follower.routeAgentMessage":
|
|
1166
1574
|
envelope = parseRouteAgentMessageEnvelope(value, requestId);
|
|
1167
1575
|
break;
|
|
1576
|
+
case "follower.deliverSelectedMenu":
|
|
1577
|
+
envelope = parseSelectedMenuDeliveryEnvelope(value, requestId);
|
|
1578
|
+
break;
|
|
1168
1579
|
case "follower.callApi":
|
|
1169
1580
|
envelope = parseCallApiEnvelope(value, requestId);
|
|
1170
1581
|
break;
|
|
@@ -1192,7 +1603,10 @@ type TelegramBusServerGlobal = typeof globalThis & {
|
|
|
1192
1603
|
[TELEGRAM_ACTIVE_LOCAL_SERVERS]?: Map<string, TelegramBusLocalServer>;
|
|
1193
1604
|
};
|
|
1194
1605
|
|
|
1195
|
-
function getActiveTelegramBusLocalServers(): Map<
|
|
1606
|
+
function getActiveTelegramBusLocalServers(): Map<
|
|
1607
|
+
string,
|
|
1608
|
+
TelegramBusLocalServer
|
|
1609
|
+
> {
|
|
1196
1610
|
const root = globalThis as TelegramBusServerGlobal;
|
|
1197
1611
|
return (root[TELEGRAM_ACTIVE_LOCAL_SERVERS] ??= new Map());
|
|
1198
1612
|
}
|
|
@@ -1247,7 +1661,22 @@ interface TelegramBusLocalServerDeps {
|
|
|
1247
1661
|
) => boolean;
|
|
1248
1662
|
}
|
|
1249
1663
|
|
|
1664
|
+
/** Local IPC issuance proof only: this callback/error never enters the envelope or certifies a remote API effect. */
|
|
1665
|
+
export class TelegramBusLocalAuthorityError extends Error {
|
|
1666
|
+
readonly requestIssued: boolean;
|
|
1667
|
+
constructor(requestIssued: boolean, reason?: string) {
|
|
1668
|
+
super(
|
|
1669
|
+
reason
|
|
1670
|
+
? `Telegram bus local authority is unavailable: ${reason}`
|
|
1671
|
+
: "Telegram bus local authority is unavailable.",
|
|
1672
|
+
);
|
|
1673
|
+
this.name = "TelegramBusLocalAuthorityError";
|
|
1674
|
+
this.requestIssued = requestIssued;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1250
1678
|
interface TelegramBusLocalClientOptions {
|
|
1679
|
+
assertAuthority?: () => void;
|
|
1251
1680
|
socketPath: string;
|
|
1252
1681
|
envelope: TelegramBusEnvelope;
|
|
1253
1682
|
timeoutMs?: number;
|
|
@@ -1265,13 +1694,21 @@ interface TelegramBusForeignOwnedForwarderDeps<TMessage = unknown> {
|
|
|
1265
1694
|
message: TMessage,
|
|
1266
1695
|
) => "comment" | "forward" | undefined;
|
|
1267
1696
|
localProtocolIdentity?: TelegramBusProtocolIdentity;
|
|
1268
|
-
validateForwardOwnership?: (
|
|
1697
|
+
validateForwardOwnership?: (
|
|
1698
|
+
ownership: TelegramBusForwardOwnership,
|
|
1699
|
+
) => boolean;
|
|
1269
1700
|
resolveInputCustodyReference?: (input: {
|
|
1270
1701
|
sourceUpdateId: number;
|
|
1271
1702
|
recipientBindingKey: string;
|
|
1272
|
-
}) =>
|
|
1273
|
-
|
|
1274
|
-
|
|
1703
|
+
}) =>
|
|
1704
|
+
| {
|
|
1705
|
+
sourceRecoveryKey: string;
|
|
1706
|
+
source: {
|
|
1707
|
+
updateId: number;
|
|
1708
|
+
owner: { acquisitionId: string; handoffId?: string };
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
| undefined;
|
|
1275
1712
|
recordRuntimeEvent?: (
|
|
1276
1713
|
category: string,
|
|
1277
1714
|
error: unknown,
|
|
@@ -1290,10 +1727,14 @@ export function isTelegramBusForwardOwnershipCurrent(
|
|
|
1290
1727
|
expected: TelegramBusForwardOwnership,
|
|
1291
1728
|
current: TelegramBusForwardOwnership | undefined,
|
|
1292
1729
|
): boolean {
|
|
1293
|
-
return Boolean(
|
|
1730
|
+
return Boolean(
|
|
1731
|
+
current &&
|
|
1732
|
+
current.instanceId === expected.instanceId &&
|
|
1294
1733
|
current.ownerGeneration === expected.ownerGeneration &&
|
|
1295
1734
|
current.recipientBindingKey === expected.recipientBindingKey &&
|
|
1296
|
-
JSON.stringify(current.protocolIdentity) ===
|
|
1735
|
+
JSON.stringify(current.protocolIdentity) ===
|
|
1736
|
+
JSON.stringify(expected.protocolIdentity),
|
|
1737
|
+
);
|
|
1297
1738
|
}
|
|
1298
1739
|
|
|
1299
1740
|
type TelegramBusDurableForwardEnvelope = Extract<
|
|
@@ -1337,6 +1778,13 @@ function createTelegramBusForwardDelivery(
|
|
|
1337
1778
|
});
|
|
1338
1779
|
}
|
|
1339
1780
|
|
|
1781
|
+
/**
|
|
1782
|
+
* Leader-to-follower forwards and control replies share the stale-liveness window: a follower doing journal work or stalled
|
|
1783
|
+
* by an ordinary multi-second Pi/TUI turn, or a slower Windows named pipe, is not a lost recipient.
|
|
1784
|
+
*/
|
|
1785
|
+
const TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS =
|
|
1786
|
+
TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
|
|
1787
|
+
|
|
1340
1788
|
export function createTelegramBusForeignOwnedUpdateForwarder<
|
|
1341
1789
|
TContext,
|
|
1342
1790
|
TReactionUpdate,
|
|
@@ -1392,14 +1840,16 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
|
|
|
1392
1840
|
envelopeKind: input.envelopeKind,
|
|
1393
1841
|
recipientInstanceId: input.ownership.instanceId,
|
|
1394
1842
|
deliveryId: input.delivery?.deliveryId,
|
|
1395
|
-
sourceUpdateId:
|
|
1396
|
-
input.delivery?.sourceUpdateId ?? input.sourceUpdateId,
|
|
1843
|
+
sourceUpdateId: input.delivery?.sourceUpdateId ?? input.sourceUpdateId,
|
|
1397
1844
|
});
|
|
1398
1845
|
return settlement;
|
|
1399
1846
|
};
|
|
1400
1847
|
const prepare = (
|
|
1401
|
-
kind:
|
|
1402
|
-
|
|
1848
|
+
kind:
|
|
1849
|
+
| "leader.forwardCallback"
|
|
1850
|
+
| "leader.forwardReaction"
|
|
1851
|
+
| "leader.forwardMessage"
|
|
1852
|
+
| "leader.forwardEditedMessage",
|
|
1403
1853
|
value: unknown,
|
|
1404
1854
|
ownership: TelegramBusForwardOwnership,
|
|
1405
1855
|
):
|
|
@@ -1434,27 +1884,48 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
|
|
|
1434
1884
|
};
|
|
1435
1885
|
}
|
|
1436
1886
|
const sourceReference = canUseTelegramBusInputCustodyReference({
|
|
1437
|
-
local: deps.localProtocolIdentity,
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
sourceRecoveryKey: reference.sourceRecoveryKey, source: reference.source })
|
|
1448
|
-
: createTelegramBusForwardDelivery(kind, sourceUpdateId, ownership.recipientBindingKey);
|
|
1449
|
-
if (!ownership.ownerGeneration) {
|
|
1887
|
+
local: deps.localProtocolIdentity,
|
|
1888
|
+
remote: ownership.protocolIdentity,
|
|
1889
|
+
});
|
|
1890
|
+
const reference = sourceReference
|
|
1891
|
+
? deps.resolveInputCustodyReference?.({
|
|
1892
|
+
sourceUpdateId,
|
|
1893
|
+
recipientBindingKey: ownership.recipientBindingKey,
|
|
1894
|
+
})
|
|
1895
|
+
: undefined;
|
|
1896
|
+
if (sourceReference && !reference)
|
|
1450
1897
|
return {
|
|
1451
1898
|
settlement: reject({
|
|
1452
1899
|
status: "retryable",
|
|
1453
|
-
failureClass: "
|
|
1454
|
-
message: "Forwarded Telegram update has no
|
|
1900
|
+
failureClass: "source-reference-missing",
|
|
1901
|
+
message: "Forwarded Telegram update has no exact custody reference.",
|
|
1455
1902
|
envelopeKind: kind,
|
|
1456
1903
|
ownership,
|
|
1457
|
-
|
|
1904
|
+
sourceUpdateId,
|
|
1905
|
+
}),
|
|
1906
|
+
};
|
|
1907
|
+
const delivery = reference
|
|
1908
|
+
? createTelegramBusFollowerSourceReferenceDeliveryIdentity({
|
|
1909
|
+
kind: "leader.wakeInputCustody",
|
|
1910
|
+
recipientBindingKey: ownership.recipientBindingKey,
|
|
1911
|
+
sourceRecoveryKey: reference.sourceRecoveryKey,
|
|
1912
|
+
source: reference.source,
|
|
1913
|
+
})
|
|
1914
|
+
: createTelegramBusForwardDelivery(
|
|
1915
|
+
kind,
|
|
1916
|
+
sourceUpdateId,
|
|
1917
|
+
ownership.recipientBindingKey,
|
|
1918
|
+
);
|
|
1919
|
+
if (!ownership.ownerGeneration) {
|
|
1920
|
+
return {
|
|
1921
|
+
settlement: reject({
|
|
1922
|
+
status: "retryable",
|
|
1923
|
+
failureClass: "recipient-generation-missing",
|
|
1924
|
+
message:
|
|
1925
|
+
"Forwarded Telegram update has no live recipient generation.",
|
|
1926
|
+
envelopeKind: kind,
|
|
1927
|
+
ownership,
|
|
1928
|
+
delivery,
|
|
1458
1929
|
}),
|
|
1459
1930
|
};
|
|
1460
1931
|
}
|
|
@@ -1468,10 +1939,18 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
|
|
|
1468
1939
|
envelope: TelegramBusDurableForwardEnvelope,
|
|
1469
1940
|
ownership: TelegramBusForwardOwnership,
|
|
1470
1941
|
): Promise<TelegramBusForeignUpdateSettlement> => {
|
|
1471
|
-
if (
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1942
|
+
if (
|
|
1943
|
+
deps.validateForwardOwnership &&
|
|
1944
|
+
!deps.validateForwardOwnership(ownership)
|
|
1945
|
+
)
|
|
1946
|
+
return reject({
|
|
1947
|
+
status: "retryable",
|
|
1948
|
+
failureClass: "recipient-ownership-stale",
|
|
1949
|
+
message: "Telegram follower ownership changed before forwarding.",
|
|
1950
|
+
envelopeKind: envelope.kind,
|
|
1951
|
+
ownership,
|
|
1952
|
+
delivery: envelope.delivery,
|
|
1953
|
+
});
|
|
1475
1954
|
if (deps.getAuthSecret) envelope.auth = deps.getAuthSecret();
|
|
1476
1955
|
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
1477
1956
|
let response: TelegramBusEnvelope | undefined;
|
|
@@ -1479,7 +1958,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
|
|
|
1479
1958
|
response = await sendTelegramBusLocalEnvelope({
|
|
1480
1959
|
socketPath,
|
|
1481
1960
|
envelope,
|
|
1482
|
-
timeoutMs: deps.timeoutMs,
|
|
1961
|
+
timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
|
|
1483
1962
|
retry: getTelegramBusTransportRetryPolicy({
|
|
1484
1963
|
endpoint: socketPath,
|
|
1485
1964
|
operation: "operation",
|
|
@@ -1551,7 +2030,8 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
|
|
|
1551
2030
|
return reject({
|
|
1552
2031
|
status: "terminal-rejected",
|
|
1553
2032
|
failureClass: "durable-receipt-mismatched",
|
|
1554
|
-
message:
|
|
2033
|
+
message:
|
|
2034
|
+
"Follower acknowledgement returned a mismatched durable receipt.",
|
|
1555
2035
|
envelopeKind: envelope.kind,
|
|
1556
2036
|
ownership,
|
|
1557
2037
|
delivery: envelope.delivery,
|
|
@@ -1559,105 +2039,88 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
|
|
|
1559
2039
|
}
|
|
1560
2040
|
return { status: "accepted", delivery: envelope.delivery };
|
|
1561
2041
|
};
|
|
2042
|
+
type ForwardKind = Parameters<typeof prepare>[0];
|
|
2043
|
+
type ForwardBase = Pick<
|
|
2044
|
+
Extract<
|
|
2045
|
+
TelegramBusDurableForwardEnvelope,
|
|
2046
|
+
{ kind: "leader.wakeInputCustody" }
|
|
2047
|
+
>,
|
|
2048
|
+
| "requestId"
|
|
2049
|
+
| "recipientInstanceId"
|
|
2050
|
+
| "recipientRegistrationGeneration"
|
|
2051
|
+
| "delivery"
|
|
2052
|
+
>;
|
|
2053
|
+
// A recipient already holding the source reference is only woken; it never receives a second payload copy.
|
|
2054
|
+
const forward = (
|
|
2055
|
+
kind: ForwardKind,
|
|
2056
|
+
value: unknown,
|
|
2057
|
+
ownership: TelegramBusForwardOwnership,
|
|
2058
|
+
build: (
|
|
2059
|
+
base: ForwardBase,
|
|
2060
|
+
sentAtMs: number,
|
|
2061
|
+
) => TelegramBusDurableForwardEnvelope,
|
|
2062
|
+
): Promise<TelegramBusForeignUpdateSettlement> => {
|
|
2063
|
+
const prepared = prepare(kind, value, ownership);
|
|
2064
|
+
if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
|
|
2065
|
+
const base = {
|
|
2066
|
+
requestId: deps.createRequestId(),
|
|
2067
|
+
recipientInstanceId: ownership.instanceId,
|
|
2068
|
+
recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
|
|
2069
|
+
delivery: prepared.delivery,
|
|
2070
|
+
};
|
|
2071
|
+
return send(
|
|
2072
|
+
prepared.sourceReference
|
|
2073
|
+
? { kind: "leader.wakeInputCustody", ...base, sentAtMs: getNowMs() }
|
|
2074
|
+
: build(base, getNowMs()),
|
|
2075
|
+
ownership,
|
|
2076
|
+
);
|
|
2077
|
+
};
|
|
1562
2078
|
return {
|
|
1563
|
-
forwardCallback: ({ query, ownership }) =>
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
kind: "leader.forwardCallback",
|
|
1573
|
-
requestId: deps.createRequestId(),
|
|
1574
|
-
recipientInstanceId: ownership.instanceId,
|
|
1575
|
-
recipientRegistrationGeneration:
|
|
1576
|
-
prepared.recipientRegistrationGeneration,
|
|
1577
|
-
delivery: prepared.delivery,
|
|
1578
|
-
query,
|
|
1579
|
-
sentAtMs: getNowMs(),
|
|
1580
|
-
},
|
|
1581
|
-
ownership,
|
|
1582
|
-
);
|
|
1583
|
-
},
|
|
1584
|
-
forwardReaction: ({ reactionUpdate, ownership }) => {
|
|
1585
|
-
const prepared = prepare(
|
|
2079
|
+
forwardCallback: ({ query, ownership }) =>
|
|
2080
|
+
forward("leader.forwardCallback", query, ownership, (base, sentAtMs) => ({
|
|
2081
|
+
kind: "leader.forwardCallback",
|
|
2082
|
+
...base,
|
|
2083
|
+
query,
|
|
2084
|
+
sentAtMs,
|
|
2085
|
+
})),
|
|
2086
|
+
forwardReaction: ({ reactionUpdate, ownership }) =>
|
|
2087
|
+
forward(
|
|
1586
2088
|
"leader.forwardReaction",
|
|
1587
2089
|
reactionUpdate,
|
|
1588
2090
|
ownership,
|
|
1589
|
-
|
|
1590
|
-
if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
|
|
1591
|
-
if (prepared.sourceReference) return send({ kind: "leader.wakeInputCustody",
|
|
1592
|
-
requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
|
|
1593
|
-
recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
|
|
1594
|
-
delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
|
|
1595
|
-
return send(
|
|
1596
|
-
{
|
|
2091
|
+
(base, sentAtMs) => ({
|
|
1597
2092
|
kind: "leader.forwardReaction",
|
|
1598
|
-
|
|
1599
|
-
recipientInstanceId: ownership.instanceId,
|
|
1600
|
-
recipientRegistrationGeneration:
|
|
1601
|
-
prepared.recipientRegistrationGeneration,
|
|
1602
|
-
delivery: prepared.delivery,
|
|
2093
|
+
...base,
|
|
1603
2094
|
reactionUpdate,
|
|
1604
|
-
sentAtMs
|
|
1605
|
-
},
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
if (prepared.sourceReference) return send({ kind: "leader.wakeInputCustody",
|
|
1613
|
-
requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
|
|
1614
|
-
recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
|
|
1615
|
-
delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
|
|
1616
|
-
return send(
|
|
1617
|
-
{
|
|
2095
|
+
sentAtMs,
|
|
2096
|
+
}),
|
|
2097
|
+
),
|
|
2098
|
+
forwardMessage: ({ message, ownership }) =>
|
|
2099
|
+
forward("leader.forwardMessage", message, ownership, (base, sentAtMs) => {
|
|
2100
|
+
const forwardCommentBatchPosition =
|
|
2101
|
+
deps.getForwardCommentBatchPosition?.(message);
|
|
2102
|
+
return {
|
|
1618
2103
|
kind: "leader.forwardMessage",
|
|
1619
|
-
|
|
1620
|
-
recipientInstanceId: ownership.instanceId,
|
|
1621
|
-
recipientRegistrationGeneration:
|
|
1622
|
-
prepared.recipientRegistrationGeneration,
|
|
1623
|
-
delivery: prepared.delivery,
|
|
2104
|
+
...base,
|
|
1624
2105
|
message,
|
|
1625
|
-
...(
|
|
1626
|
-
? {
|
|
1627
|
-
forwardCommentBatchPosition:
|
|
1628
|
-
deps.getForwardCommentBatchPosition(message),
|
|
1629
|
-
}
|
|
2106
|
+
...(forwardCommentBatchPosition !== undefined
|
|
2107
|
+
? { forwardCommentBatchPosition }
|
|
1630
2108
|
: {}),
|
|
1631
|
-
sentAtMs
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
forwardEditedMessage: ({ message, ownership }) => {
|
|
1637
|
-
const prepared = prepare(
|
|
2109
|
+
sentAtMs,
|
|
2110
|
+
};
|
|
2111
|
+
}),
|
|
2112
|
+
forwardEditedMessage: ({ message, ownership }) =>
|
|
2113
|
+
forward(
|
|
1638
2114
|
"leader.forwardEditedMessage",
|
|
1639
2115
|
message,
|
|
1640
2116
|
ownership,
|
|
1641
|
-
|
|
1642
|
-
if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
|
|
1643
|
-
if (prepared.sourceReference) return send({ kind: "leader.wakeInputCustody",
|
|
1644
|
-
requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
|
|
1645
|
-
recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
|
|
1646
|
-
delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
|
|
1647
|
-
return send(
|
|
1648
|
-
{
|
|
2117
|
+
(base, sentAtMs) => ({
|
|
1649
2118
|
kind: "leader.forwardEditedMessage",
|
|
1650
|
-
|
|
1651
|
-
recipientInstanceId: ownership.instanceId,
|
|
1652
|
-
recipientRegistrationGeneration:
|
|
1653
|
-
prepared.recipientRegistrationGeneration,
|
|
1654
|
-
delivery: prepared.delivery,
|
|
2119
|
+
...base,
|
|
1655
2120
|
message,
|
|
1656
|
-
sentAtMs
|
|
1657
|
-
},
|
|
1658
|
-
|
|
1659
|
-
);
|
|
1660
|
-
},
|
|
2121
|
+
sentAtMs,
|
|
2122
|
+
}),
|
|
2123
|
+
),
|
|
1661
2124
|
};
|
|
1662
2125
|
}
|
|
1663
2126
|
|
|
@@ -1683,50 +2146,573 @@ export function createTelegramBusWorkspaceRestoreController(deps: {
|
|
|
1683
2146
|
getAuthSecret: () => string | undefined;
|
|
1684
2147
|
timeoutMs?: number;
|
|
1685
2148
|
}) {
|
|
1686
|
-
return async (input: {
|
|
1687
|
-
|
|
1688
|
-
|
|
2149
|
+
return async (input: {
|
|
2150
|
+
operationId: string;
|
|
2151
|
+
instanceId: string;
|
|
2152
|
+
sessionId: string;
|
|
2153
|
+
slot: string;
|
|
2154
|
+
target: TelegramTarget & { threadId: number };
|
|
2155
|
+
oldTarget: TelegramTarget & { threadId: number };
|
|
2156
|
+
mode: "apply" | "inspect";
|
|
2157
|
+
isCurrent: () => boolean;
|
|
2158
|
+
}): Promise<TelegramBusWorkspaceRestoreObservation | undefined> => {
|
|
1689
2159
|
const isCurrent = input.isCurrent.bind(input);
|
|
1690
2160
|
if (!isCurrent()) return undefined;
|
|
1691
2161
|
const follower = deps.getFollower(input.instanceId);
|
|
1692
2162
|
const secret = deps.getAuthSecret();
|
|
1693
|
-
if (
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2163
|
+
if (
|
|
2164
|
+
!secret ||
|
|
2165
|
+
!follower?.registrationGeneration ||
|
|
2166
|
+
!follower.busSocketPath ||
|
|
2167
|
+
follower.sessionId !== input.sessionId ||
|
|
2168
|
+
follower.slot !== input.slot ||
|
|
2169
|
+
!hasTelegramBusSharedCapabilities(
|
|
2170
|
+
deps.localProtocolIdentity,
|
|
2171
|
+
follower.protocol,
|
|
2172
|
+
[TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE],
|
|
2173
|
+
)
|
|
2174
|
+
)
|
|
2175
|
+
return undefined;
|
|
1698
2176
|
const captured = structuredClone(follower);
|
|
1699
|
-
const expected = {
|
|
1700
|
-
|
|
2177
|
+
const expected = {
|
|
2178
|
+
operationId: input.operationId,
|
|
2179
|
+
sessionId: input.sessionId,
|
|
2180
|
+
slot: input.slot,
|
|
2181
|
+
target: { ...input.target },
|
|
2182
|
+
oldTarget: { ...input.oldTarget },
|
|
2183
|
+
mode: input.mode,
|
|
2184
|
+
};
|
|
1701
2185
|
const current = (): boolean => {
|
|
1702
|
-
if (
|
|
1703
|
-
|
|
2186
|
+
if (
|
|
2187
|
+
!isCurrent() ||
|
|
2188
|
+
deps.getAuthSecret() !== secret ||
|
|
2189
|
+
!hasTelegramBusCapability(
|
|
2190
|
+
deps.localProtocolIdentity,
|
|
2191
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
2192
|
+
)
|
|
2193
|
+
)
|
|
2194
|
+
return false;
|
|
1704
2195
|
const live = deps.getFollower(captured.instanceId);
|
|
1705
|
-
return
|
|
1706
|
-
live
|
|
1707
|
-
live.
|
|
2196
|
+
return (
|
|
2197
|
+
!!live &&
|
|
2198
|
+
live.registrationGeneration === captured.registrationGeneration &&
|
|
2199
|
+
live.sessionId === captured.sessionId &&
|
|
2200
|
+
live.cwd === captured.cwd &&
|
|
2201
|
+
live.slot === captured.slot &&
|
|
2202
|
+
live.busSocketPath === captured.busSocketPath &&
|
|
2203
|
+
live.target?.chatId === captured.target?.chatId &&
|
|
1708
2204
|
live.target?.threadId === captured.target?.threadId &&
|
|
1709
|
-
hasTelegramBusCapability(
|
|
1710
|
-
|
|
2205
|
+
hasTelegramBusCapability(
|
|
2206
|
+
live.protocol,
|
|
2207
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
|
|
2208
|
+
) &&
|
|
2209
|
+
getTelegramBusProtocolCompatibility({
|
|
2210
|
+
local: deps.localProtocolIdentity,
|
|
2211
|
+
remote: live.protocol,
|
|
2212
|
+
}).compatible
|
|
2213
|
+
);
|
|
1711
2214
|
};
|
|
1712
2215
|
if (!current()) return undefined;
|
|
1713
2216
|
const requestId = deps.createRequestId();
|
|
1714
|
-
const response = await sendTelegramBusLocalEnvelope({
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
2217
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
2218
|
+
socketPath: captured.busSocketPath!,
|
|
2219
|
+
timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
|
|
2220
|
+
retry: { attempts: 1, delayMs: 0 },
|
|
2221
|
+
envelope: {
|
|
2222
|
+
kind: "leader.workspaceRestore",
|
|
2223
|
+
requestId,
|
|
2224
|
+
recipientInstanceId: captured.instanceId,
|
|
2225
|
+
recipientRegistrationGeneration: captured.registrationGeneration!,
|
|
2226
|
+
operationId: expected.operationId,
|
|
2227
|
+
mode: expected.mode,
|
|
2228
|
+
auth: secret,
|
|
2229
|
+
sentAtMs: Date.now(),
|
|
2230
|
+
},
|
|
2231
|
+
});
|
|
2232
|
+
if (
|
|
2233
|
+
!current() ||
|
|
2234
|
+
response?.kind !== "bus.ack" ||
|
|
2235
|
+
response.requestId !== requestId ||
|
|
2236
|
+
response.ok !== true ||
|
|
2237
|
+
!response.result ||
|
|
2238
|
+
typeof response.result !== "object"
|
|
2239
|
+
)
|
|
2240
|
+
return undefined;
|
|
1720
2241
|
const result = response.result as Record<string, unknown>;
|
|
1721
2242
|
const recipient = result.recipient as Record<string, unknown> | undefined;
|
|
1722
2243
|
const target = parseThreadTarget(result.target);
|
|
1723
2244
|
const wanted = result.ready === true ? expected.target : expected.oldTarget;
|
|
1724
|
-
if (
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
2245
|
+
if (
|
|
2246
|
+
result.operationId !== expected.operationId ||
|
|
2247
|
+
typeof result.ready !== "boolean" ||
|
|
2248
|
+
result.slot !== expected.slot ||
|
|
2249
|
+
!target ||
|
|
2250
|
+
target.chatId !== wanted.chatId ||
|
|
2251
|
+
target.threadId !== wanted.threadId ||
|
|
2252
|
+
recipient?.kind !== "follower" ||
|
|
2253
|
+
recipient.instanceId !== captured.instanceId ||
|
|
2254
|
+
recipient.sessionId !== expected.sessionId ||
|
|
2255
|
+
recipient.generation !== captured.registrationGeneration
|
|
2256
|
+
)
|
|
2257
|
+
return undefined;
|
|
2258
|
+
return {
|
|
2259
|
+
operationId: expected.operationId,
|
|
2260
|
+
recipient: {
|
|
2261
|
+
kind: "follower",
|
|
2262
|
+
instanceId: captured.instanceId,
|
|
2263
|
+
sessionId: expected.sessionId,
|
|
2264
|
+
generation: captured.registrationGeneration!,
|
|
2265
|
+
},
|
|
2266
|
+
target,
|
|
2267
|
+
slot: expected.slot,
|
|
2268
|
+
ready: result.ready,
|
|
2269
|
+
};
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
/** Each capability has its own effect boundary; transport retries never replay save/dispatch/disposal. */
|
|
2274
|
+
export function createTelegramBusLiveRebindController(deps: {
|
|
2275
|
+
getFollower(instanceId: string): TelegramBusFollowerView | undefined;
|
|
2276
|
+
localProtocolIdentity: TelegramBusProtocolIdentity;
|
|
2277
|
+
createRequestId(): string;
|
|
2278
|
+
getAuthSecret(): string | undefined;
|
|
2279
|
+
timeoutMs?: number;
|
|
2280
|
+
}) {
|
|
2281
|
+
return async (
|
|
2282
|
+
input: {
|
|
2283
|
+
operationId: string;
|
|
2284
|
+
instanceId: string;
|
|
2285
|
+
sessionId: string;
|
|
2286
|
+
recipientBindingKey: string;
|
|
2287
|
+
isCurrent(): boolean;
|
|
2288
|
+
selectedCommand?: TelegramBusSelectedCommandInput;
|
|
2289
|
+
preparedSource?: TelegramBusPreparedCommandSource;
|
|
2290
|
+
} & (
|
|
2291
|
+
| { updates: ({ update_id: number } & Record<string, unknown>)[] }
|
|
2292
|
+
| {
|
|
2293
|
+
mode:
|
|
2294
|
+
| "apply"
|
|
2295
|
+
| "inspect"
|
|
2296
|
+
| "release"
|
|
2297
|
+
| "discard"
|
|
2298
|
+
| "observe"
|
|
2299
|
+
| "observe-command";
|
|
2300
|
+
sourceUpdateIds: number[];
|
|
2301
|
+
slot: string;
|
|
2302
|
+
target: TelegramTarget & { threadId: number };
|
|
2303
|
+
oldTarget: TelegramTarget & { threadId: number };
|
|
2304
|
+
}
|
|
2305
|
+
),
|
|
2306
|
+
): Promise<
|
|
2307
|
+
| TelegramBusLiveRebindSaveObservation
|
|
2308
|
+
| TelegramBusLiveRebindApplyObservation
|
|
2309
|
+
| TelegramBusLiveRebindSettlementObservation
|
|
2310
|
+
| TelegramBusLiveRebindWorkObservation
|
|
2311
|
+
| TelegramBusLiveRebindCommandObservation
|
|
2312
|
+
| undefined
|
|
2313
|
+
> => {
|
|
2314
|
+
const isCurrent = input.isCurrent.bind(input),
|
|
2315
|
+
localProtocol = structuredClone(deps.localProtocolIdentity);
|
|
2316
|
+
const selectedCommand = parseSelectedCommandInput(input.selectedCommand),
|
|
2317
|
+
held = selectedCommand,
|
|
2318
|
+
preparedSource = parsePreparedCommandSource(input.preparedSource);
|
|
2319
|
+
if (
|
|
2320
|
+
!hasOnlyWireKeys(input, [
|
|
2321
|
+
"operationId",
|
|
2322
|
+
"instanceId",
|
|
2323
|
+
"sessionId",
|
|
2324
|
+
"recipientBindingKey",
|
|
2325
|
+
"isCurrent",
|
|
2326
|
+
"selectedCommand",
|
|
2327
|
+
"preparedSource",
|
|
2328
|
+
"updates",
|
|
2329
|
+
"mode",
|
|
2330
|
+
"sourceUpdateIds",
|
|
2331
|
+
"slot",
|
|
2332
|
+
"target",
|
|
2333
|
+
"oldTarget",
|
|
2334
|
+
]) ||
|
|
2335
|
+
("mode" in input &&
|
|
2336
|
+
(![
|
|
2337
|
+
"apply",
|
|
2338
|
+
"inspect",
|
|
2339
|
+
"release",
|
|
2340
|
+
"discard",
|
|
2341
|
+
"observe",
|
|
2342
|
+
"observe-command",
|
|
2343
|
+
].includes(input.mode) ||
|
|
2344
|
+
(input.mode === "observe-command" && !selectedCommand)))
|
|
2345
|
+
)
|
|
2346
|
+
return undefined;
|
|
2347
|
+
if (
|
|
2348
|
+
(input.preparedSource !== undefined &&
|
|
2349
|
+
(!held ||
|
|
2350
|
+
!preparedSource ||
|
|
2351
|
+
preparedSource.journalBindingKey !== input.recipientBindingKey ||
|
|
2352
|
+
preparedSource.updateId !==
|
|
2353
|
+
("mode" in input
|
|
2354
|
+
? input.sourceUpdateIds[0]
|
|
2355
|
+
: input.updates[0]?.update_id))) ||
|
|
2356
|
+
(held && "mode" in input && !preparedSource)
|
|
2357
|
+
)
|
|
2358
|
+
return undefined;
|
|
2359
|
+
if (
|
|
2360
|
+
input.selectedCommand !== undefined &&
|
|
2361
|
+
(!held ||
|
|
2362
|
+
("mode" in input
|
|
2363
|
+
? input.sourceUpdateIds.length !== 1 ||
|
|
2364
|
+
!areTelegramTargetsEqual(input.target, held.target)
|
|
2365
|
+
: input.updates.length !== 1))
|
|
2366
|
+
)
|
|
2367
|
+
return undefined;
|
|
2368
|
+
const marker = selectedCommand ? { selectedCommand } : {};
|
|
2369
|
+
const expected = structuredClone({
|
|
2370
|
+
operationId: input.operationId,
|
|
2371
|
+
instanceId: input.instanceId,
|
|
2372
|
+
sessionId: input.sessionId,
|
|
2373
|
+
recipientBindingKey: input.recipientBindingKey,
|
|
2374
|
+
...(held
|
|
2375
|
+
? { ...marker, ...(preparedSource ? { preparedSource } : {}) }
|
|
2376
|
+
: {}),
|
|
2377
|
+
...("mode" in input
|
|
2378
|
+
? {
|
|
2379
|
+
mode: input.mode,
|
|
2380
|
+
sourceUpdateIds: input.sourceUpdateIds,
|
|
2381
|
+
slot: input.slot,
|
|
2382
|
+
target: input.target,
|
|
2383
|
+
oldTarget: input.oldTarget,
|
|
2384
|
+
}
|
|
2385
|
+
: { updates: input.updates }),
|
|
2386
|
+
});
|
|
2387
|
+
const observing = "mode" in expected && expected.mode === "observe";
|
|
2388
|
+
const commandObserving =
|
|
2389
|
+
"mode" in expected && expected.mode === "observe-command";
|
|
2390
|
+
const settling =
|
|
2391
|
+
"mode" in expected &&
|
|
2392
|
+
(expected.mode === "release" ||
|
|
2393
|
+
expected.mode === "discard" ||
|
|
2394
|
+
observing ||
|
|
2395
|
+
commandObserving);
|
|
2396
|
+
const required = [
|
|
2397
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
|
|
2398
|
+
...("mode" in expected || held
|
|
2399
|
+
? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY]
|
|
2400
|
+
: []),
|
|
2401
|
+
...(settling || held ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
|
|
2402
|
+
...(held
|
|
2403
|
+
? [
|
|
2404
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
2405
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
2406
|
+
]
|
|
2407
|
+
: []),
|
|
2408
|
+
];
|
|
2409
|
+
const follower = deps.getFollower(expected.instanceId),
|
|
2410
|
+
secret = deps.getAuthSecret();
|
|
2411
|
+
if (
|
|
2412
|
+
!isCurrent() ||
|
|
2413
|
+
!secret ||
|
|
2414
|
+
!follower?.registrationGeneration ||
|
|
2415
|
+
!follower.busSocketPath ||
|
|
2416
|
+
follower.sessionId !== expected.sessionId ||
|
|
2417
|
+
!hasTelegramBusSharedCapabilities(
|
|
2418
|
+
deps.localProtocolIdentity,
|
|
2419
|
+
follower.protocol,
|
|
2420
|
+
required,
|
|
2421
|
+
)
|
|
2422
|
+
)
|
|
2423
|
+
return undefined;
|
|
2424
|
+
const captured = structuredClone(follower);
|
|
2425
|
+
/** The first failed authority condition, by fixed label; undefined while the captured recipient is current. */
|
|
2426
|
+
const staleReason = (): string | undefined => {
|
|
2427
|
+
if (!isCurrent()) return "caller authority";
|
|
2428
|
+
if (deps.getAuthSecret() !== secret) return "auth secret";
|
|
2429
|
+
if (
|
|
2430
|
+
!isDeepStrictEqual(deps.localProtocolIdentity, localProtocol) ||
|
|
2431
|
+
!required.every((cap) =>
|
|
2432
|
+
hasTelegramBusCapability(deps.localProtocolIdentity, cap),
|
|
2433
|
+
)
|
|
2434
|
+
)
|
|
2435
|
+
return "local protocol";
|
|
2436
|
+
const live = deps.getFollower(expected.instanceId);
|
|
2437
|
+
if (!live) return "follower registration";
|
|
2438
|
+
if (
|
|
2439
|
+
live.sessionId !== captured.sessionId ||
|
|
2440
|
+
live.registrationGeneration !== captured.registrationGeneration ||
|
|
2441
|
+
live.sessionGeneration !== captured.sessionGeneration
|
|
2442
|
+
)
|
|
2443
|
+
return "follower generation";
|
|
2444
|
+
if (
|
|
2445
|
+
live.pid !== captured.pid ||
|
|
2446
|
+
live.processBirthId !== captured.processBirthId
|
|
2447
|
+
)
|
|
2448
|
+
return "follower process";
|
|
2449
|
+
if (
|
|
2450
|
+
live.busSocketPath !== captured.busSocketPath ||
|
|
2451
|
+
live.slot !== captured.slot ||
|
|
2452
|
+
live.cwd !== captured.cwd
|
|
2453
|
+
)
|
|
2454
|
+
return "follower endpoint";
|
|
2455
|
+
if (
|
|
2456
|
+
"mode" in expected
|
|
2457
|
+
? live.slot !== expected.slot ||
|
|
2458
|
+
!live.target ||
|
|
2459
|
+
(!areTelegramTargetsEqual(live.target, expected.target!) &&
|
|
2460
|
+
!areTelegramTargetsEqual(live.target, expected.oldTarget!))
|
|
2461
|
+
: !isDeepStrictEqual(live.target, captured.target)
|
|
2462
|
+
)
|
|
2463
|
+
return "follower target";
|
|
2464
|
+
if (
|
|
2465
|
+
!required.every((cap) =>
|
|
2466
|
+
hasTelegramBusCapability(live.protocol, cap),
|
|
2467
|
+
) ||
|
|
2468
|
+
!isDeepStrictEqual(live.protocol, captured.protocol) ||
|
|
2469
|
+
!getTelegramBusProtocolCompatibility({
|
|
2470
|
+
local: deps.localProtocolIdentity,
|
|
2471
|
+
remote: live.protocol,
|
|
2472
|
+
}).compatible
|
|
2473
|
+
)
|
|
2474
|
+
return "follower protocol";
|
|
2475
|
+
return undefined;
|
|
2476
|
+
};
|
|
2477
|
+
const current = () => staleReason() === undefined;
|
|
2478
|
+
if (!current()) return undefined;
|
|
2479
|
+
const requestId = deps.createRequestId();
|
|
2480
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
2481
|
+
socketPath: captured.busSocketPath!,
|
|
2482
|
+
timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
|
|
2483
|
+
retry: { attempts: 1, delayMs: 0 },
|
|
2484
|
+
...(held
|
|
2485
|
+
? {
|
|
2486
|
+
assertAuthority: () => {
|
|
2487
|
+
const reason = staleReason();
|
|
2488
|
+
if (reason)
|
|
2489
|
+
throw new Error(
|
|
2490
|
+
`Selected held input sender authority changed (${reason}).`,
|
|
2491
|
+
);
|
|
2492
|
+
},
|
|
2493
|
+
}
|
|
2494
|
+
: {}),
|
|
2495
|
+
envelope: {
|
|
2496
|
+
requestId,
|
|
2497
|
+
auth: secret,
|
|
2498
|
+
...(held
|
|
2499
|
+
? {
|
|
2500
|
+
...marker,
|
|
2501
|
+
...("mode" in expected
|
|
2502
|
+
? { preparedSource: expected.preparedSource }
|
|
2503
|
+
: {}),
|
|
2504
|
+
}
|
|
2505
|
+
: {}),
|
|
2506
|
+
recipientInstanceId: expected.instanceId,
|
|
2507
|
+
recipientRegistrationGeneration: captured.registrationGeneration!,
|
|
2508
|
+
recipientSessionId: expected.sessionId,
|
|
2509
|
+
recipientBindingKey: expected.recipientBindingKey,
|
|
2510
|
+
operationId: expected.operationId,
|
|
2511
|
+
sentAtMs: Date.now(),
|
|
2512
|
+
...("mode" in expected
|
|
2513
|
+
? settling
|
|
2514
|
+
? {
|
|
2515
|
+
kind: "leader.settleLiveRebind" as const,
|
|
2516
|
+
mode: expected.mode as
|
|
2517
|
+
"release" | "discard" | "observe" | "observe-command",
|
|
2518
|
+
sourceUpdateIds: expected.sourceUpdateIds!,
|
|
2519
|
+
...(observing ? { oldTarget: expected.oldTarget } : {}),
|
|
2520
|
+
}
|
|
2521
|
+
: {
|
|
2522
|
+
kind: "leader.applyLiveRebind" as const,
|
|
2523
|
+
mode: expected.mode as "apply" | "inspect",
|
|
2524
|
+
sourceUpdateIds: expected.sourceUpdateIds!,
|
|
2525
|
+
}
|
|
2526
|
+
: {
|
|
2527
|
+
kind: "leader.prepareLiveRebind" as const,
|
|
2528
|
+
updates: expected.updates!,
|
|
2529
|
+
}),
|
|
2530
|
+
},
|
|
2531
|
+
});
|
|
2532
|
+
if (
|
|
2533
|
+
!current() ||
|
|
2534
|
+
response?.kind !== "bus.ack" ||
|
|
2535
|
+
response.requestId !== requestId ||
|
|
2536
|
+
response.ok !== true ||
|
|
2537
|
+
!isRecord(response.result)
|
|
2538
|
+
)
|
|
2539
|
+
return undefined;
|
|
2540
|
+
const result = response.result,
|
|
2541
|
+
recipient = result.recipient;
|
|
2542
|
+
if (
|
|
2543
|
+
!hasOnlyWireKeys(result, [
|
|
2544
|
+
"operationId",
|
|
2545
|
+
"recipient",
|
|
2546
|
+
"sourceUpdateIds",
|
|
2547
|
+
"selectedCommand",
|
|
2548
|
+
"preparedSource",
|
|
2549
|
+
"status",
|
|
2550
|
+
"command",
|
|
2551
|
+
"sourceAck",
|
|
2552
|
+
"target",
|
|
2553
|
+
"slot",
|
|
2554
|
+
"action",
|
|
2555
|
+
"oldTarget",
|
|
2556
|
+
"work",
|
|
2557
|
+
])
|
|
2558
|
+
)
|
|
2559
|
+
return undefined;
|
|
2560
|
+
if (
|
|
2561
|
+
selectedCommand
|
|
2562
|
+
? !isDeepStrictEqual(
|
|
2563
|
+
parseSelectedCommandInput(result.selectedCommand),
|
|
2564
|
+
selectedCommand,
|
|
2565
|
+
)
|
|
2566
|
+
: Object.hasOwn(result, "selectedCommand")
|
|
2567
|
+
)
|
|
2568
|
+
return undefined;
|
|
2569
|
+
const ids = (
|
|
2570
|
+
"mode" in expected
|
|
2571
|
+
? [...expected.sourceUpdateIds!]
|
|
2572
|
+
: expected.updates!.map((update) => update.update_id)
|
|
2573
|
+
).sort((a, b) => a - b);
|
|
2574
|
+
const observedSource = parsePreparedCommandSource(result.preparedSource);
|
|
2575
|
+
if (
|
|
2576
|
+
held
|
|
2577
|
+
? !observedSource ||
|
|
2578
|
+
observedSource.updateId !== ids[0] ||
|
|
2579
|
+
observedSource.journalBindingKey !== expected.recipientBindingKey ||
|
|
2580
|
+
(expected.preparedSource &&
|
|
2581
|
+
!isDeepStrictEqual(observedSource, expected.preparedSource))
|
|
2582
|
+
: Object.hasOwn(result, "preparedSource")
|
|
2583
|
+
)
|
|
2584
|
+
return undefined;
|
|
2585
|
+
if (
|
|
2586
|
+
result.operationId !== expected.operationId ||
|
|
2587
|
+
!(commandObserving
|
|
2588
|
+
? result.status === "command-observed"
|
|
2589
|
+
: observing
|
|
2590
|
+
? result.status === "observed"
|
|
2591
|
+
: settling
|
|
2592
|
+
? result.status === "protected" ||
|
|
2593
|
+
result.status === "unknown" ||
|
|
2594
|
+
result.status ===
|
|
2595
|
+
(expected.mode === "release" ? "released" : "discarded")
|
|
2596
|
+
: "mode" in expected
|
|
2597
|
+
? result.status === "saved" || result.status === "applied"
|
|
2598
|
+
: result.status === "saved") ||
|
|
2599
|
+
!isRecord(recipient) ||
|
|
2600
|
+
recipient.instanceId !== expected.instanceId ||
|
|
2601
|
+
recipient.sessionId !== expected.sessionId ||
|
|
2602
|
+
recipient.generation !== captured.registrationGeneration ||
|
|
2603
|
+
recipient.bindingKey !== expected.recipientBindingKey ||
|
|
2604
|
+
!Array.isArray(result.sourceUpdateIds) ||
|
|
2605
|
+
result.sourceUpdateIds.length !== ids.length ||
|
|
2606
|
+
result.sourceUpdateIds.some((id, index) => id !== ids[index])
|
|
2607
|
+
)
|
|
2608
|
+
return undefined;
|
|
2609
|
+
const observation: TelegramBusLiveRebindSaveObservation = {
|
|
2610
|
+
operationId: expected.operationId,
|
|
2611
|
+
recipient: {
|
|
2612
|
+
instanceId: expected.instanceId,
|
|
2613
|
+
sessionId: expected.sessionId,
|
|
2614
|
+
generation: captured.registrationGeneration!,
|
|
2615
|
+
bindingKey: expected.recipientBindingKey,
|
|
2616
|
+
},
|
|
2617
|
+
sourceUpdateIds: ids,
|
|
2618
|
+
...(held
|
|
2619
|
+
? { ...structuredClone(marker), preparedSource: { ...observedSource! } }
|
|
2620
|
+
: {}),
|
|
2621
|
+
status: "saved",
|
|
2622
|
+
};
|
|
2623
|
+
if (!("mode" in expected)) return observation;
|
|
2624
|
+
if (commandObserving) {
|
|
2625
|
+
const sourceAck = parsePreparedCommandSource(result.sourceAck);
|
|
2626
|
+
if (
|
|
2627
|
+
!hasOnlyWireKeys(result, [
|
|
2628
|
+
"operationId",
|
|
2629
|
+
"recipient",
|
|
2630
|
+
"sourceUpdateIds",
|
|
2631
|
+
"selectedCommand",
|
|
2632
|
+
"preparedSource",
|
|
2633
|
+
"status",
|
|
2634
|
+
"command",
|
|
2635
|
+
"sourceAck",
|
|
2636
|
+
]) ||
|
|
2637
|
+
!hasOnlyWireKeys(recipient, [
|
|
2638
|
+
"instanceId",
|
|
2639
|
+
"sessionId",
|
|
2640
|
+
"generation",
|
|
2641
|
+
"bindingKey",
|
|
2642
|
+
]) ||
|
|
2643
|
+
!["completed", "unknown", "not-issued"].includes(
|
|
2644
|
+
result.command as string,
|
|
2645
|
+
) ||
|
|
2646
|
+
(Object.hasOwn(result, "sourceAck") &&
|
|
2647
|
+
(!sourceAck ||
|
|
2648
|
+
!isDeepStrictEqual(sourceAck, expected.preparedSource) ||
|
|
2649
|
+
result.command === "not-issued"))
|
|
2650
|
+
)
|
|
2651
|
+
return undefined;
|
|
2652
|
+
const settled = {
|
|
2653
|
+
command:
|
|
2654
|
+
result.command as TelegramBusLiveRebindCommandObservation["command"],
|
|
2655
|
+
...(sourceAck ? { sourceAck: { ...sourceAck } } : {}),
|
|
2656
|
+
};
|
|
2657
|
+
return {
|
|
2658
|
+
...observation,
|
|
2659
|
+
status: "command-observed",
|
|
2660
|
+
selectedCommand: structuredClone(selectedCommand!),
|
|
2661
|
+
preparedSource: { ...observedSource! },
|
|
2662
|
+
...settled,
|
|
2663
|
+
};
|
|
2664
|
+
}
|
|
2665
|
+
if (observing) {
|
|
2666
|
+
const work = result.work,
|
|
2667
|
+
oldTarget = parseThreadTarget(result.oldTarget);
|
|
2668
|
+
if (
|
|
2669
|
+
!oldTarget ||
|
|
2670
|
+
!isDeepStrictEqual(oldTarget, expected.oldTarget) ||
|
|
2671
|
+
!isRecord(work) ||
|
|
2672
|
+
[
|
|
2673
|
+
work.sessionBusy,
|
|
2674
|
+
work.targetWork,
|
|
2675
|
+
work.deliveryPending,
|
|
2676
|
+
work.unknown,
|
|
2677
|
+
].some((flag) => typeof flag !== "boolean")
|
|
2678
|
+
)
|
|
2679
|
+
return undefined;
|
|
2680
|
+
return {
|
|
2681
|
+
...observation,
|
|
2682
|
+
status: "observed",
|
|
2683
|
+
oldTarget,
|
|
2684
|
+
work: {
|
|
2685
|
+
sessionBusy: work.sessionBusy as boolean,
|
|
2686
|
+
targetWork: work.targetWork as boolean,
|
|
2687
|
+
deliveryPending: work.deliveryPending as boolean,
|
|
2688
|
+
unknown: work.unknown as boolean,
|
|
2689
|
+
},
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
if (settling) {
|
|
2693
|
+
if (result.action !== expected.mode) return undefined;
|
|
2694
|
+
return {
|
|
2695
|
+
...observation,
|
|
2696
|
+
action: expected.mode as "release" | "discard",
|
|
2697
|
+
status:
|
|
2698
|
+
result.status as TelegramBusLiveRebindSettlementObservation["status"],
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
const target = parseThreadTarget(result.target),
|
|
2702
|
+
wanted =
|
|
2703
|
+
result.status === "applied" ? expected.target : expected.oldTarget;
|
|
2704
|
+
if (
|
|
2705
|
+
!target ||
|
|
2706
|
+
!isDeepStrictEqual(target, wanted) ||
|
|
2707
|
+
result.slot !== expected.slot
|
|
2708
|
+
)
|
|
2709
|
+
return undefined;
|
|
2710
|
+
return {
|
|
2711
|
+
...observation,
|
|
2712
|
+
status: result.status as "saved" | "applied",
|
|
2713
|
+
target,
|
|
2714
|
+
slot: expected.slot!,
|
|
2715
|
+
};
|
|
1730
2716
|
};
|
|
1731
2717
|
}
|
|
1732
2718
|
|
|
@@ -1741,15 +2727,21 @@ export function isTelegramBusEnvelopeAuthorized(
|
|
|
1741
2727
|
return auth.length === expected.length && timingSafeEqual(auth, expected);
|
|
1742
2728
|
}
|
|
1743
2729
|
|
|
2730
|
+
/** Negative acknowledgement carrying only a diagnostic message. */
|
|
2731
|
+
export function rejectTelegramBusRequest(
|
|
2732
|
+
requestId: string,
|
|
2733
|
+
message: string,
|
|
2734
|
+
): Extract<TelegramBusEnvelope, { kind: "bus.ack" }> {
|
|
2735
|
+
return { kind: "bus.ack", requestId, ok: false, message };
|
|
2736
|
+
}
|
|
2737
|
+
|
|
1744
2738
|
export function createUnauthorizedBusAck(
|
|
1745
2739
|
requestId: string,
|
|
1746
2740
|
): TelegramBusEnvelope {
|
|
1747
|
-
return
|
|
1748
|
-
kind: "bus.ack",
|
|
2741
|
+
return rejectTelegramBusRequest(
|
|
1749
2742
|
requestId,
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
};
|
|
2743
|
+
"Unauthorized Telegram bus envelope.",
|
|
2744
|
+
);
|
|
1753
2745
|
}
|
|
1754
2746
|
|
|
1755
2747
|
export function createTelegramBusLocalServer(
|
|
@@ -1814,7 +2806,9 @@ export function createTelegramBusLocalServer(
|
|
|
1814
2806
|
result: Promise.resolve().then(() => deps.handleEnvelope(envelope)),
|
|
1815
2807
|
};
|
|
1816
2808
|
requestLedger.set(key, entry);
|
|
1817
|
-
const settled = () => {
|
|
2809
|
+
const settled = () => {
|
|
2810
|
+
entry.settled = true;
|
|
2811
|
+
};
|
|
1818
2812
|
void entry.result.then(settled, settled);
|
|
1819
2813
|
return entry.result;
|
|
1820
2814
|
};
|
|
@@ -2070,6 +3064,25 @@ function sendTelegramBusLocalEnvelopeOnce(
|
|
|
2070
3064
|
): Promise<TelegramBusEnvelope | undefined> {
|
|
2071
3065
|
const timeoutMs = options.timeoutMs ?? 1000;
|
|
2072
3066
|
return new Promise((resolve, reject) => {
|
|
3067
|
+
const assertAuthority = options.assertAuthority;
|
|
3068
|
+
let issued = false;
|
|
3069
|
+
const assertCurrent = () => {
|
|
3070
|
+
try {
|
|
3071
|
+
assertAuthority?.();
|
|
3072
|
+
} catch (error) {
|
|
3073
|
+
// The caller's reason is a fixed condition label, never request or credential content.
|
|
3074
|
+
throw new TelegramBusLocalAuthorityError(
|
|
3075
|
+
issued,
|
|
3076
|
+
error instanceof Error ? error.message : undefined,
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
};
|
|
3080
|
+
try {
|
|
3081
|
+
assertCurrent();
|
|
3082
|
+
} catch (error) {
|
|
3083
|
+
reject(error);
|
|
3084
|
+
return;
|
|
3085
|
+
}
|
|
2073
3086
|
const socket = createConnection(options.socketPath);
|
|
2074
3087
|
let settled = false;
|
|
2075
3088
|
let buffer = "";
|
|
@@ -2102,14 +3115,28 @@ function sendTelegramBusLocalEnvelopeOnce(
|
|
|
2102
3115
|
}, timeoutMs);
|
|
2103
3116
|
socket.setEncoding("utf8");
|
|
2104
3117
|
socket.once("connect", () => {
|
|
2105
|
-
|
|
3118
|
+
try {
|
|
3119
|
+
assertCurrent();
|
|
3120
|
+
const encoded = encodeTelegramBusEnvelope(options.envelope);
|
|
3121
|
+
assertCurrent();
|
|
3122
|
+
issued = true;
|
|
3123
|
+
socket.write(encoded);
|
|
3124
|
+
} catch (error) {
|
|
3125
|
+
settle(() => reject(error));
|
|
3126
|
+
}
|
|
2106
3127
|
});
|
|
2107
3128
|
socket.on("data", (chunk) => {
|
|
2108
3129
|
buffer += chunk;
|
|
2109
3130
|
const newlineIndex = buffer.indexOf("\n");
|
|
2110
3131
|
if (newlineIndex < 0) return;
|
|
2111
3132
|
const line = buffer.slice(0, newlineIndex);
|
|
2112
|
-
|
|
3133
|
+
try {
|
|
3134
|
+
const response = parseTelegramBusEnvelope(line);
|
|
3135
|
+
assertCurrent();
|
|
3136
|
+
settle(() => resolve(response));
|
|
3137
|
+
} catch (error) {
|
|
3138
|
+
settle(() => reject(error));
|
|
3139
|
+
}
|
|
2113
3140
|
});
|
|
2114
3141
|
socket.once("error", (error) => settle(() => reject(error)));
|
|
2115
3142
|
socket.once("end", () => settle(() => resolve(undefined)));
|
|
@@ -2137,7 +3164,11 @@ export async function sendTelegramBusLocalEnvelope(
|
|
|
2137
3164
|
attempts,
|
|
2138
3165
|
...info,
|
|
2139
3166
|
});
|
|
2140
|
-
if (
|
|
3167
|
+
if (
|
|
3168
|
+
error instanceof TelegramBusLocalAuthorityError ||
|
|
3169
|
+
attempt >= attempts ||
|
|
3170
|
+
!isRetryableTelegramBusTransportError(error)
|
|
3171
|
+
) {
|
|
2141
3172
|
throw error;
|
|
2142
3173
|
}
|
|
2143
3174
|
resolvedOptions.recordTransportEvent?.("client-retry", {
|
|
@@ -2179,15 +3210,21 @@ export interface TelegramBusFollowerRegistry {
|
|
|
2179
3210
|
export function createTelegramBusForwardOwnershipValidator(
|
|
2180
3211
|
registry: Pick<TelegramBusFollowerRegistry, "get">,
|
|
2181
3212
|
): (ownership: TelegramBusForwardOwnership) => boolean {
|
|
2182
|
-
return ownership => {
|
|
3213
|
+
return (ownership) => {
|
|
2183
3214
|
const follower = registry.get(ownership.instanceId);
|
|
2184
|
-
return isTelegramBusForwardOwnershipCurrent(
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
3215
|
+
return isTelegramBusForwardOwnershipCurrent(
|
|
3216
|
+
ownership,
|
|
3217
|
+
follower?.registrationGeneration &&
|
|
3218
|
+
follower.profileKey &&
|
|
3219
|
+
follower.protocol
|
|
3220
|
+
? {
|
|
3221
|
+
instanceId: follower.instanceId,
|
|
3222
|
+
ownerGeneration: follower.registrationGeneration,
|
|
3223
|
+
recipientBindingKey: follower.profileKey,
|
|
3224
|
+
protocolIdentity: follower.protocol,
|
|
3225
|
+
}
|
|
3226
|
+
: undefined,
|
|
3227
|
+
);
|
|
2191
3228
|
};
|
|
2192
3229
|
}
|
|
2193
3230
|
|
|
@@ -2195,15 +3232,22 @@ function hasTelegramBusFollowerIdentityOverlap(
|
|
|
2195
3232
|
first: TelegramBusInstanceRegistration,
|
|
2196
3233
|
second: TelegramBusInstanceRegistration,
|
|
2197
3234
|
): boolean {
|
|
2198
|
-
return
|
|
2199
|
-
|
|
2200
|
-
(first.
|
|
2201
|
-
first.
|
|
3235
|
+
return (
|
|
3236
|
+
first.instanceId === second.instanceId ||
|
|
3237
|
+
(first.profileKey !== undefined &&
|
|
3238
|
+
first.profileKey === second.profileKey) ||
|
|
3239
|
+
(first.target !== undefined &&
|
|
3240
|
+
first.target.chatId === second.target?.chatId &&
|
|
3241
|
+
first.target.threadId === second.target.threadId)
|
|
3242
|
+
);
|
|
2202
3243
|
}
|
|
2203
3244
|
|
|
2204
3245
|
export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry {
|
|
2205
3246
|
const followers = new Map<string, TelegramBusFollowerView>();
|
|
2206
|
-
const observations = new Set<{
|
|
3247
|
+
const observations = new Set<{
|
|
3248
|
+
follower: TelegramBusFollowerView;
|
|
3249
|
+
current: boolean;
|
|
3250
|
+
}>();
|
|
2207
3251
|
const clone = (
|
|
2208
3252
|
follower: TelegramBusFollowerView,
|
|
2209
3253
|
): TelegramBusFollowerView => ({
|
|
@@ -2223,7 +3267,8 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
|
|
|
2223
3267
|
const existing = followers.get(registration.instanceId);
|
|
2224
3268
|
for (const [instanceId, follower] of followers.entries()) {
|
|
2225
3269
|
if (instanceId === registration.instanceId) continue;
|
|
2226
|
-
if (hasTelegramBusFollowerIdentityOverlap(registration, follower))
|
|
3270
|
+
if (hasTelegramBusFollowerIdentityOverlap(registration, follower))
|
|
3271
|
+
followers.delete(instanceId);
|
|
2227
3272
|
}
|
|
2228
3273
|
const next: TelegramBusFollowerView = {
|
|
2229
3274
|
...registration,
|
|
@@ -2241,7 +3286,8 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
|
|
|
2241
3286
|
};
|
|
2242
3287
|
followers.set(registration.instanceId, next);
|
|
2243
3288
|
for (const observation of observations) {
|
|
2244
|
-
if (!hasTelegramBusFollowerIdentityOverlap(next, observation.follower))
|
|
3289
|
+
if (!hasTelegramBusFollowerIdentityOverlap(next, observation.follower))
|
|
3290
|
+
continue;
|
|
2245
3291
|
observation.current = false;
|
|
2246
3292
|
observations.delete(observation);
|
|
2247
3293
|
}
|
|
@@ -2278,8 +3324,12 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
|
|
|
2278
3324
|
},
|
|
2279
3325
|
observeUnregistered: (follower) => {
|
|
2280
3326
|
// This watches replacement, not process death or delivery authority.
|
|
2281
|
-
const observation = {
|
|
2282
|
-
|
|
3327
|
+
const observation = {
|
|
3328
|
+
follower: clone(follower),
|
|
3329
|
+
current: ![...followers.values()].some((current) =>
|
|
3330
|
+
hasTelegramBusFollowerIdentityOverlap(current, follower),
|
|
3331
|
+
),
|
|
3332
|
+
};
|
|
2283
3333
|
if (observation.current) observations.add(observation);
|
|
2284
3334
|
return {
|
|
2285
3335
|
isCurrent: () => observation.current,
|
|
@@ -2340,12 +3390,12 @@ async function handleTelegramBusSocketLine(
|
|
|
2340
3390
|
...classifyTelegramBusTransportError(error),
|
|
2341
3391
|
});
|
|
2342
3392
|
socket.write(
|
|
2343
|
-
encodeTelegramBusEnvelope(
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
3393
|
+
encodeTelegramBusEnvelope(
|
|
3394
|
+
rejectTelegramBusRequest(
|
|
3395
|
+
envelope.requestId,
|
|
3396
|
+
"Telegram bus handler failed.",
|
|
3397
|
+
),
|
|
3398
|
+
),
|
|
2349
3399
|
);
|
|
2350
3400
|
}
|
|
2351
3401
|
}
|
|
@@ -2359,32 +3409,15 @@ function parseRegisterEnvelope(
|
|
|
2359
3409
|
return registration ? { kind, requestId, registration } : undefined;
|
|
2360
3410
|
}
|
|
2361
3411
|
|
|
2362
|
-
function
|
|
2363
|
-
value: Record<string, unknown>,
|
|
2364
|
-
requestId: string,
|
|
2365
|
-
): TelegramBusEnvelope | undefined {
|
|
2366
|
-
return typeof value.instanceId === "string" &&
|
|
2367
|
-
typeof value.sentAtMs === "number"
|
|
2368
|
-
? {
|
|
2369
|
-
kind: "follower.heartbeat",
|
|
2370
|
-
requestId,
|
|
2371
|
-
instanceId: value.instanceId,
|
|
2372
|
-
...(typeof value.registrationGeneration === "string"
|
|
2373
|
-
? { registrationGeneration: value.registrationGeneration }
|
|
2374
|
-
: {}),
|
|
2375
|
-
sentAtMs: value.sentAtMs,
|
|
2376
|
-
}
|
|
2377
|
-
: undefined;
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
function parseDisconnectEnvelope(
|
|
3412
|
+
function parseFollowerPresenceEnvelope(
|
|
2381
3413
|
value: Record<string, unknown>,
|
|
2382
3414
|
requestId: string,
|
|
3415
|
+
kind: "follower.heartbeat" | "follower.disconnect",
|
|
2383
3416
|
): TelegramBusEnvelope | undefined {
|
|
2384
3417
|
return typeof value.instanceId === "string" &&
|
|
2385
3418
|
typeof value.sentAtMs === "number"
|
|
2386
3419
|
? {
|
|
2387
|
-
kind
|
|
3420
|
+
kind,
|
|
2388
3421
|
requestId,
|
|
2389
3422
|
instanceId: value.instanceId,
|
|
2390
3423
|
...(typeof value.registrationGeneration === "string"
|
|
@@ -2429,14 +3462,17 @@ function parseTelegramBusFollowerDeliveryIdentity(
|
|
|
2429
3462
|
typeof value.recipientBindingKey !== "string" ||
|
|
2430
3463
|
!value.recipientBindingKey ||
|
|
2431
3464
|
(value.sourceRecoveryKey !== undefined &&
|
|
2432
|
-
(typeof value.sourceRecoveryKey !== "string" ||
|
|
3465
|
+
(typeof value.sourceRecoveryKey !== "string" ||
|
|
3466
|
+
!value.sourceRecoveryKey ||
|
|
2433
3467
|
value.sourceRecoveryKey.length > 1_024)) ||
|
|
2434
3468
|
(value.sourceClaim !== undefined &&
|
|
2435
3469
|
(!isRecord(value.sourceClaim) ||
|
|
2436
3470
|
typeof value.sourceClaim.acquisitionId !== "string" ||
|
|
2437
|
-
!value.sourceClaim.acquisitionId ||
|
|
3471
|
+
!value.sourceClaim.acquisitionId ||
|
|
3472
|
+
value.sourceClaim.acquisitionId.length > 256 ||
|
|
2438
3473
|
typeof value.sourceClaim.handoffId !== "string" ||
|
|
2439
|
-
!value.sourceClaim.handoffId ||
|
|
3474
|
+
!value.sourceClaim.handoffId ||
|
|
3475
|
+
value.sourceClaim.handoffId.length > 256))
|
|
2440
3476
|
) {
|
|
2441
3477
|
return undefined;
|
|
2442
3478
|
}
|
|
@@ -2445,10 +3481,16 @@ function parseTelegramBusFollowerDeliveryIdentity(
|
|
|
2445
3481
|
sourceUpdateId: value.sourceUpdateId as number,
|
|
2446
3482
|
recipientBindingKey: value.recipientBindingKey,
|
|
2447
3483
|
...(typeof value.sourceRecoveryKey === "string"
|
|
2448
|
-
? { sourceRecoveryKey: value.sourceRecoveryKey }
|
|
3484
|
+
? { sourceRecoveryKey: value.sourceRecoveryKey }
|
|
3485
|
+
: {}),
|
|
2449
3486
|
...(isRecord(value.sourceClaim)
|
|
2450
|
-
? {
|
|
2451
|
-
|
|
3487
|
+
? {
|
|
3488
|
+
sourceClaim: {
|
|
3489
|
+
acquisitionId: value.sourceClaim.acquisitionId as string,
|
|
3490
|
+
handoffId: value.sourceClaim.handoffId as string,
|
|
3491
|
+
},
|
|
3492
|
+
}
|
|
3493
|
+
: {}),
|
|
2452
3494
|
};
|
|
2453
3495
|
}
|
|
2454
3496
|
|
|
@@ -2524,42 +3566,68 @@ function parseForwardMessageEnvelope(
|
|
|
2524
3566
|
}
|
|
2525
3567
|
|
|
2526
3568
|
function parseOfferInputCustodyHandoffEnvelope(
|
|
2527
|
-
value: Record<string, unknown>,
|
|
3569
|
+
value: Record<string, unknown>,
|
|
3570
|
+
requestId: string,
|
|
2528
3571
|
): TelegramBusEnvelope | undefined {
|
|
2529
3572
|
const source = value.source;
|
|
2530
|
-
if (
|
|
3573
|
+
if (
|
|
3574
|
+
!isRecord(source) ||
|
|
3575
|
+
typeof value.recipientInstanceId !== "string" ||
|
|
2531
3576
|
typeof value.recipientRegistrationGeneration !== "string" ||
|
|
2532
|
-
typeof value.recipientBindingKey !== "string" ||
|
|
2533
|
-
|
|
3577
|
+
typeof value.recipientBindingKey !== "string" ||
|
|
3578
|
+
!value.recipientBindingKey ||
|
|
3579
|
+
typeof value.sourceRecoveryKey !== "string" ||
|
|
3580
|
+
!value.sourceRecoveryKey ||
|
|
2534
3581
|
value.sourceRecoveryKey.length > 1_024 ||
|
|
2535
|
-
typeof source.journalBindingKey !== "string" ||
|
|
3582
|
+
typeof source.journalBindingKey !== "string" ||
|
|
3583
|
+
!source.journalBindingKey ||
|
|
2536
3584
|
source.journalBindingKey !== value.sourceRecoveryKey ||
|
|
2537
|
-
typeof source.tokenSha256 !== "string" ||
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
3585
|
+
typeof source.tokenSha256 !== "string" ||
|
|
3586
|
+
!/^[a-f0-9]{64}$/u.test(source.tokenSha256) ||
|
|
3587
|
+
!Number.isSafeInteger(source.updateId) ||
|
|
3588
|
+
(source.updateId as number) < 0 ||
|
|
3589
|
+
typeof value.handoffId !== "string" ||
|
|
3590
|
+
!value.handoffId ||
|
|
3591
|
+
value.handoffId.length > 256 ||
|
|
3592
|
+
typeof value.sentAtMs !== "number"
|
|
3593
|
+
)
|
|
3594
|
+
return undefined;
|
|
3595
|
+
return {
|
|
3596
|
+
kind: "leader.offerInputCustodyHandoff",
|
|
3597
|
+
requestId,
|
|
2542
3598
|
recipientInstanceId: value.recipientInstanceId,
|
|
2543
3599
|
recipientRegistrationGeneration: value.recipientRegistrationGeneration,
|
|
2544
3600
|
recipientBindingKey: value.recipientBindingKey,
|
|
2545
3601
|
sourceRecoveryKey: value.sourceRecoveryKey,
|
|
2546
|
-
source: {
|
|
2547
|
-
|
|
2548
|
-
|
|
3602
|
+
source: {
|
|
3603
|
+
journalBindingKey: source.journalBindingKey,
|
|
3604
|
+
tokenSha256: source.tokenSha256,
|
|
3605
|
+
updateId: source.updateId as number,
|
|
3606
|
+
},
|
|
3607
|
+
handoffId: value.handoffId,
|
|
3608
|
+
sentAtMs: value.sentAtMs,
|
|
3609
|
+
};
|
|
2549
3610
|
}
|
|
2550
3611
|
|
|
2551
3612
|
function parseWakeInputCustodyEnvelope(
|
|
2552
|
-
value: Record<string, unknown>,
|
|
3613
|
+
value: Record<string, unknown>,
|
|
3614
|
+
requestId: string,
|
|
2553
3615
|
): TelegramBusEnvelope | undefined {
|
|
2554
3616
|
const delivery = parseTelegramBusFollowerDeliveryIdentity(value.delivery);
|
|
2555
|
-
return delivery &&
|
|
3617
|
+
return delivery &&
|
|
3618
|
+
delivery.sourceRecoveryKey &&
|
|
3619
|
+
delivery.sourceClaim &&
|
|
2556
3620
|
typeof value.recipientInstanceId === "string" &&
|
|
2557
3621
|
typeof value.recipientRegistrationGeneration === "string" &&
|
|
2558
3622
|
typeof value.sentAtMs === "number"
|
|
2559
|
-
? {
|
|
3623
|
+
? {
|
|
3624
|
+
kind: "leader.wakeInputCustody",
|
|
3625
|
+
requestId,
|
|
2560
3626
|
recipientInstanceId: value.recipientInstanceId,
|
|
2561
3627
|
recipientRegistrationGeneration: value.recipientRegistrationGeneration,
|
|
2562
|
-
delivery,
|
|
3628
|
+
delivery,
|
|
3629
|
+
sentAtMs: value.sentAtMs,
|
|
3630
|
+
}
|
|
2563
3631
|
: undefined;
|
|
2564
3632
|
}
|
|
2565
3633
|
|
|
@@ -2710,6 +3778,171 @@ function parseRouteAgentMessageEnvelope(
|
|
|
2710
3778
|
};
|
|
2711
3779
|
}
|
|
2712
3780
|
|
|
3781
|
+
const nonempty = (input: unknown, limit = 128): input is string =>
|
|
3782
|
+
typeof input === "string" && !!input.trim() && input.length <= limit;
|
|
3783
|
+
const positive = (input: unknown): input is number =>
|
|
3784
|
+
Number.isSafeInteger(input) && (input as number) > 0;
|
|
3785
|
+
const nonnegative = (input: unknown): input is number =>
|
|
3786
|
+
Number.isSafeInteger(input) && (input as number) >= 0;
|
|
3787
|
+
|
|
3788
|
+
/** Shared closed text-effect codec; parsing alone supplies no recipient or issuance authority. */
|
|
3789
|
+
export function parseTelegramBusSelectedMenuTextEffect(
|
|
3790
|
+
effect: unknown,
|
|
3791
|
+
): TelegramBusSelectedMenuTextEffect | undefined {
|
|
3792
|
+
if (
|
|
3793
|
+
!isRecord(effect) ||
|
|
3794
|
+
(effect.kind !== "send-text" && effect.kind !== "edit-text") ||
|
|
3795
|
+
!hasOnlyWireKeys(effect, [
|
|
3796
|
+
"kind",
|
|
3797
|
+
"text",
|
|
3798
|
+
"parseMode",
|
|
3799
|
+
"replyMarkup",
|
|
3800
|
+
...(effect.kind === "send-text" ? ["replyToMessageId"] : ["messageId"]),
|
|
3801
|
+
]) ||
|
|
3802
|
+
!nonempty(effect.text, 4096) ||
|
|
3803
|
+
(effect.parseMode !== undefined && effect.parseMode !== "HTML") ||
|
|
3804
|
+
(effect.kind === "send-text"
|
|
3805
|
+
? effect.replyToMessageId !== undefined &&
|
|
3806
|
+
!positive(effect.replyToMessageId)
|
|
3807
|
+
: !positive(effect.messageId))
|
|
3808
|
+
)
|
|
3809
|
+
return undefined;
|
|
3810
|
+
let replyMarkup: TelegramBusSelectedMenuTextEffect["replyMarkup"];
|
|
3811
|
+
if (effect.replyMarkup !== undefined) {
|
|
3812
|
+
const markup = effect.replyMarkup;
|
|
3813
|
+
if (
|
|
3814
|
+
!isRecord(markup) ||
|
|
3815
|
+
!hasOnlyWireKeys(markup, ["inline_keyboard"]) ||
|
|
3816
|
+
!Array.isArray(markup.inline_keyboard) ||
|
|
3817
|
+
markup.inline_keyboard.length > 32 ||
|
|
3818
|
+
markup.inline_keyboard.some(
|
|
3819
|
+
(row) =>
|
|
3820
|
+
!Array.isArray(row) ||
|
|
3821
|
+
!row.length ||
|
|
3822
|
+
row.length > 8 ||
|
|
3823
|
+
row.some(
|
|
3824
|
+
(button) =>
|
|
3825
|
+
!isRecord(button) ||
|
|
3826
|
+
!hasOnlyWireKeys(button, ["text", "callback_data"]) ||
|
|
3827
|
+
!nonempty(button.text) ||
|
|
3828
|
+
!nonempty(button.callback_data, 64) ||
|
|
3829
|
+
Buffer.byteLength(button.callback_data, "utf8") > 64,
|
|
3830
|
+
),
|
|
3831
|
+
)
|
|
3832
|
+
)
|
|
3833
|
+
return undefined;
|
|
3834
|
+
replyMarkup = {
|
|
3835
|
+
inline_keyboard: markup.inline_keyboard.map((row) =>
|
|
3836
|
+
(row as { text: string; callback_data: string }[]).map((button) => ({
|
|
3837
|
+
text: button.text,
|
|
3838
|
+
callback_data: button.callback_data,
|
|
3839
|
+
})),
|
|
3840
|
+
),
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
const common = {
|
|
3844
|
+
text: effect.text,
|
|
3845
|
+
...(effect.parseMode === "HTML" ? { parseMode: "HTML" as const } : {}),
|
|
3846
|
+
...(replyMarkup ? { replyMarkup } : {}),
|
|
3847
|
+
};
|
|
3848
|
+
return effect.kind === "send-text"
|
|
3849
|
+
? {
|
|
3850
|
+
...common,
|
|
3851
|
+
kind: "send-text",
|
|
3852
|
+
...(effect.replyToMessageId !== undefined
|
|
3853
|
+
? { replyToMessageId: effect.replyToMessageId as number }
|
|
3854
|
+
: {}),
|
|
3855
|
+
}
|
|
3856
|
+
: { ...common, kind: "edit-text", messageId: effect.messageId as number };
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
function parseSelectedMenuDeliveryEnvelope(
|
|
3860
|
+
value: Record<string, unknown>,
|
|
3861
|
+
requestId: string,
|
|
3862
|
+
): TelegramBusSelectedMenuDelivery | undefined {
|
|
3863
|
+
const recipient = value.recipient,
|
|
3864
|
+
executor = value.executor,
|
|
3865
|
+
effect = parseTelegramBusSelectedMenuTextEffect(value.effect);
|
|
3866
|
+
if (
|
|
3867
|
+
!hasOnlyWireKeys(value, [
|
|
3868
|
+
"kind",
|
|
3869
|
+
"requestId",
|
|
3870
|
+
"auth",
|
|
3871
|
+
"instanceId",
|
|
3872
|
+
"registrationGeneration",
|
|
3873
|
+
"operationId",
|
|
3874
|
+
"recipient",
|
|
3875
|
+
"executor",
|
|
3876
|
+
"operatorUserId",
|
|
3877
|
+
"effect",
|
|
3878
|
+
"sentAtMs",
|
|
3879
|
+
]) ||
|
|
3880
|
+
!nonempty(requestId) ||
|
|
3881
|
+
!nonempty(value.instanceId) ||
|
|
3882
|
+
!nonempty(value.registrationGeneration) ||
|
|
3883
|
+
!nonempty(value.operationId) ||
|
|
3884
|
+
!isRecord(recipient) ||
|
|
3885
|
+
!hasOnlyWireKeys(recipient, [
|
|
3886
|
+
"sessionId",
|
|
3887
|
+
"sessionGeneration",
|
|
3888
|
+
"processId",
|
|
3889
|
+
"processBirthId",
|
|
3890
|
+
"profileKey",
|
|
3891
|
+
"journalBindingKey",
|
|
3892
|
+
"target",
|
|
3893
|
+
]) ||
|
|
3894
|
+
!nonempty(recipient.sessionId) ||
|
|
3895
|
+
!nonnegative(recipient.sessionGeneration) ||
|
|
3896
|
+
!positive(recipient.processId) ||
|
|
3897
|
+
!nonempty(recipient.processBirthId) ||
|
|
3898
|
+
!nonempty(recipient.profileKey, 1024) ||
|
|
3899
|
+
!nonempty(recipient.journalBindingKey, 8192) ||
|
|
3900
|
+
recipient.profileKey === recipient.journalBindingKey ||
|
|
3901
|
+
!isRecord(recipient.target) ||
|
|
3902
|
+
!hasOnlyWireKeys(recipient.target, ["chatId", "threadId"]) ||
|
|
3903
|
+
!positive(recipient.target.chatId) ||
|
|
3904
|
+
!positive(recipient.target.threadId) ||
|
|
3905
|
+
!positive(value.operatorUserId) ||
|
|
3906
|
+
value.operatorUserId !== recipient.target.chatId ||
|
|
3907
|
+
!isRecord(executor) ||
|
|
3908
|
+
!hasOnlyWireKeys(executor, ["instanceId", "leaderEpoch"]) ||
|
|
3909
|
+
!nonempty(executor.instanceId) ||
|
|
3910
|
+
!nonempty(executor.leaderEpoch) ||
|
|
3911
|
+
!effect ||
|
|
3912
|
+
typeof value.sentAtMs !== "number" ||
|
|
3913
|
+
!Number.isFinite(value.sentAtMs) ||
|
|
3914
|
+
value.sentAtMs < 0 ||
|
|
3915
|
+
(value.auth !== undefined && typeof value.auth !== "string")
|
|
3916
|
+
)
|
|
3917
|
+
return undefined;
|
|
3918
|
+
return {
|
|
3919
|
+
kind: "follower.deliverSelectedMenu",
|
|
3920
|
+
requestId,
|
|
3921
|
+
instanceId: value.instanceId,
|
|
3922
|
+
registrationGeneration: value.registrationGeneration,
|
|
3923
|
+
operationId: value.operationId,
|
|
3924
|
+
recipient: {
|
|
3925
|
+
sessionId: recipient.sessionId,
|
|
3926
|
+
sessionGeneration: recipient.sessionGeneration,
|
|
3927
|
+
processId: recipient.processId,
|
|
3928
|
+
processBirthId: recipient.processBirthId,
|
|
3929
|
+
profileKey: recipient.profileKey,
|
|
3930
|
+
journalBindingKey: recipient.journalBindingKey,
|
|
3931
|
+
target: {
|
|
3932
|
+
chatId: recipient.target.chatId,
|
|
3933
|
+
threadId: recipient.target.threadId,
|
|
3934
|
+
},
|
|
3935
|
+
},
|
|
3936
|
+
executor: {
|
|
3937
|
+
instanceId: executor.instanceId,
|
|
3938
|
+
leaderEpoch: executor.leaderEpoch,
|
|
3939
|
+
},
|
|
3940
|
+
operatorUserId: value.operatorUserId,
|
|
3941
|
+
effect,
|
|
3942
|
+
sentAtMs: value.sentAtMs,
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3945
|
+
|
|
2713
3946
|
function parseCallApiEnvelope(
|
|
2714
3947
|
value: Record<string, unknown>,
|
|
2715
3948
|
requestId: string,
|
|
@@ -2839,9 +4072,13 @@ function parseRegistration(
|
|
|
2839
4072
|
}
|
|
2840
4073
|
if (typeof value.cwd === "string") registration.cwd = value.cwd;
|
|
2841
4074
|
if (value.sessionId !== undefined) {
|
|
2842
|
-
if (
|
|
2843
|
-
|
|
2844
|
-
|
|
4075
|
+
if (
|
|
4076
|
+
typeof value.sessionId !== "string" ||
|
|
4077
|
+
!value.sessionId ||
|
|
4078
|
+
value.sessionId !== value.sessionId.trim() ||
|
|
4079
|
+
Buffer.byteLength(value.sessionId, "utf8") > 256
|
|
4080
|
+
)
|
|
4081
|
+
return undefined;
|
|
2845
4082
|
registration.sessionId = value.sessionId;
|
|
2846
4083
|
}
|
|
2847
4084
|
if (typeof value.pid === "number") registration.pid = value.pid;
|
|
@@ -2866,14 +4103,6 @@ function parseRegistration(
|
|
|
2866
4103
|
return registration;
|
|
2867
4104
|
}
|
|
2868
4105
|
|
|
2869
|
-
function parseTarget(value: unknown): TelegramTarget | undefined {
|
|
2870
|
-
if (value === undefined) return undefined;
|
|
2871
|
-
if (!isRecord(value) || typeof value.chatId !== "number") return undefined;
|
|
2872
|
-
return typeof value.threadId === "number"
|
|
2873
|
-
? { chatId: value.chatId, threadId: value.threadId }
|
|
2874
|
-
: { chatId: value.chatId };
|
|
2875
|
-
}
|
|
2876
|
-
|
|
2877
4106
|
function parseThreadTarget(
|
|
2878
4107
|
value: unknown,
|
|
2879
4108
|
): (TelegramTarget & { threadId: number }) | undefined {
|