@llblab/pi-telegram 0.52.2 → 0.53.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +15 -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 +65 -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 +252 -127
- 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 +128 -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 +516 -301
- 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/dist/lib/bus.js
CHANGED
|
@@ -4,95 +4,21 @@
|
|
|
4
4
|
* Owns serializable bus envelopes, socket/auth helpers, local IPC client/server primitives,
|
|
5
5
|
* cross-instance forwarding helpers, and the live follower registry model.
|
|
6
6
|
*/
|
|
7
|
-
import { execFileSync } from "node:child_process";
|
|
8
7
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
9
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync,
|
|
10
|
-
import { createConnection, createServer, } from "node:net";
|
|
8
|
+
import { chmodSync, existsSync, lstatSync, mkdirSync, readlinkSync, renameSync, symlinkSync, unlinkSync, } from "node:fs";
|
|
11
9
|
import { createRequire } from "node:module";
|
|
10
|
+
import { createConnection, createServer, } from "node:net";
|
|
12
11
|
import { platform as getPlatform, tmpdir } from "node:os";
|
|
13
12
|
import { basename, dirname, join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { isWireRecord as isRecord } from "./wire.js";
|
|
13
|
+
import { isDeepStrictEqual } from "node:util";
|
|
14
|
+
import { classifyTelegramBusTransportError, createTelegramBusTransportTimeoutError, delayTelegramBusTransportRetry, getTelegramBusEndpointDiagnostics, getTelegramBusFollowerEndpoint, getTelegramBusLeaderEndpoint, getTelegramBusPipePath, getTelegramBusTransportRetryPolicy, isRetryableTelegramBusTransportError, isTelegramBusPipePath, probeTelegramBusEndpoint, TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES, } from "./bus-transport.js";
|
|
15
|
+
import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS } from "./locks.js";
|
|
18
16
|
import { resolveAgentDir } from "./paths.js";
|
|
17
|
+
import { getTelegramProcessBirthIdentity } from "./process-identity.js";
|
|
18
|
+
import { parseTelegramQueueHandoffPayload, TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES, } from "./queue.js";
|
|
19
|
+
import { areTelegramTargetsEqual, parseTelegramTarget as parseTarget, } from "./target.js";
|
|
19
20
|
import { normalizeTelegramSessionReplacementIntent, } from "./threads.js";
|
|
20
|
-
|
|
21
|
-
return execFileSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
22
|
-
encoding: "utf8",
|
|
23
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
24
|
-
}).trim();
|
|
25
|
-
}
|
|
26
|
-
function getTelegramProcessBirthProof(pid, options = {}) {
|
|
27
|
-
if (pid <= 0)
|
|
28
|
-
return { status: "unverifiable" };
|
|
29
|
-
const platform = options.platform ?? getPlatform();
|
|
30
|
-
if (platform === "linux") {
|
|
31
|
-
try {
|
|
32
|
-
const stat = (options.readProcStat ?? ((targetPid) => readFileSync(`/proc/${targetPid}/stat`, "utf8")))(pid);
|
|
33
|
-
const closeParen = stat.lastIndexOf(")");
|
|
34
|
-
const fields = stat
|
|
35
|
-
.slice(closeParen + 2)
|
|
36
|
-
.trim()
|
|
37
|
-
.split(/\s+/u);
|
|
38
|
-
const startTicks = fields[19];
|
|
39
|
-
if (startTicks) {
|
|
40
|
-
return { status: "proven", identity: `${pid}:start:${startTicks}` };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
/* inaccessible process metadata */
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
else if (platform === "darwin") {
|
|
48
|
-
try {
|
|
49
|
-
const startedAt = (options.readDarwinProcessStart ?? readDarwinProcessStart)(pid);
|
|
50
|
-
if (startedAt) {
|
|
51
|
-
const fingerprint = createHash("sha256")
|
|
52
|
-
.update(startedAt)
|
|
53
|
-
.digest("hex")
|
|
54
|
-
.slice(0, 16);
|
|
55
|
-
return { status: "proven", identity: `${pid}:start:${fingerprint}` };
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
/* inaccessible process metadata */
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return { status: "unverifiable" };
|
|
63
|
-
}
|
|
64
|
-
export function getTelegramProcessBirthIdentity(pid, fallbackGeneration, options = {}) {
|
|
65
|
-
const proof = getTelegramProcessBirthProof(pid, options);
|
|
66
|
-
return proof.status === "proven"
|
|
67
|
-
? proof.identity
|
|
68
|
-
: `${pid}:generation:${fallbackGeneration}`;
|
|
69
|
-
}
|
|
70
|
-
export function getTelegramProcessLiveness(owner, options = {}) {
|
|
71
|
-
const processAlive = options.isProcessAlive ?? isProcessAlive;
|
|
72
|
-
if (!processAlive(owner.processId))
|
|
73
|
-
return "dead";
|
|
74
|
-
const proof = getTelegramProcessBirthProof(owner.processId, options);
|
|
75
|
-
if (proof.status === "unverifiable")
|
|
76
|
-
return "unverifiable";
|
|
77
|
-
return proof.identity === owner.processBirthId ? "alive" : "dead";
|
|
78
|
-
}
|
|
79
|
-
export function getTelegramProcessBirthIdentityLiveness(processBirthId, options = {}) {
|
|
80
|
-
const match = /^(\d+):(start|generation):(.+)$/u.exec(processBirthId);
|
|
81
|
-
if (!match)
|
|
82
|
-
return "unverifiable";
|
|
83
|
-
const processId = Number(match[1]);
|
|
84
|
-
if (!Number.isSafeInteger(processId) || processId <= 0)
|
|
85
|
-
return "unverifiable";
|
|
86
|
-
const processAlive = options.isProcessAlive ?? isProcessAlive;
|
|
87
|
-
if (!processAlive(processId))
|
|
88
|
-
return "dead";
|
|
89
|
-
if (match[2] === "generation")
|
|
90
|
-
return "unverifiable";
|
|
91
|
-
const proof = getTelegramProcessBirthProof(processId, options);
|
|
92
|
-
if (proof.status === "unverifiable")
|
|
93
|
-
return "unverifiable";
|
|
94
|
-
return proof.identity === processBirthId ? "alive" : "dead";
|
|
95
|
-
}
|
|
21
|
+
import { hasOnlyWireKeys, isWireRecord as isRecord } from "./wire.js";
|
|
96
22
|
export function createCurrentTelegramBusProcessRuntime(input) {
|
|
97
23
|
return createTelegramBusProcessRuntime({
|
|
98
24
|
getActiveProfileName: input.getActiveProfileName,
|
|
@@ -130,6 +56,90 @@ export const TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT = "directory-displ
|
|
|
130
56
|
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT = "workspace-follower-auto-connect-v1";
|
|
131
57
|
export const TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT = "session-replacement-intent-v1";
|
|
132
58
|
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE = "workspace-restore-v1";
|
|
59
|
+
export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE = "live-thread-rebind-save-v1";
|
|
60
|
+
export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY = "live-thread-rebind-apply-v1";
|
|
61
|
+
export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE = "live-thread-rebind-settle-v1";
|
|
62
|
+
/** Restricted recipient text/menu delivery; negotiated identity and captured effect authority remain required. */
|
|
63
|
+
export const TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY = "selected-menu-delivery-v1";
|
|
64
|
+
/** Versioned held-command set; syntax is shared, while the captured recipient Commands registry owns supported plans. */
|
|
65
|
+
export const TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET = "live-thread-rebind-command-set-v1";
|
|
66
|
+
/** Every live rebinding needs the save/apply/settle trio; held command selections additionally need these two. */
|
|
67
|
+
export const TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES = [
|
|
68
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
|
|
69
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
|
|
70
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
|
|
71
|
+
];
|
|
72
|
+
export const TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES = [
|
|
73
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
74
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
75
|
+
];
|
|
76
|
+
function parseSelectedCommandInput(value) {
|
|
77
|
+
if (!isRecord(value) ||
|
|
78
|
+
!hasOnlyWireKeys(value, ["name", "target"]) ||
|
|
79
|
+
typeof value.name !== "string" ||
|
|
80
|
+
!/^[a-z0-9_]{1,32}$/u.test(value.name) ||
|
|
81
|
+
!isRecord(value.target) ||
|
|
82
|
+
!hasOnlyWireKeys(value.target, ["chatId", "threadId"]))
|
|
83
|
+
return undefined;
|
|
84
|
+
const target = parseThreadTarget(value.target);
|
|
85
|
+
return target &&
|
|
86
|
+
Number.isSafeInteger(target.chatId) &&
|
|
87
|
+
target.chatId > 0 &&
|
|
88
|
+
Number.isSafeInteger(target.threadId) &&
|
|
89
|
+
target.threadId > 0
|
|
90
|
+
? { name: value.name, target }
|
|
91
|
+
: undefined;
|
|
92
|
+
}
|
|
93
|
+
/** Pure live-rebind recipient/operation identity shared by prepare/apply/settle envelopes. */
|
|
94
|
+
function parseLiveRebindRecipient(value) {
|
|
95
|
+
const { recipientInstanceId, recipientRegistrationGeneration, recipientSessionId, recipientBindingKey, operationId, } = value;
|
|
96
|
+
return typeof recipientInstanceId === "string" &&
|
|
97
|
+
recipientInstanceId &&
|
|
98
|
+
typeof recipientRegistrationGeneration === "string" &&
|
|
99
|
+
recipientRegistrationGeneration &&
|
|
100
|
+
typeof recipientSessionId === "string" &&
|
|
101
|
+
recipientSessionId &&
|
|
102
|
+
typeof recipientBindingKey === "string" &&
|
|
103
|
+
recipientBindingKey &&
|
|
104
|
+
typeof operationId === "string" &&
|
|
105
|
+
operationId.length > 0 &&
|
|
106
|
+
operationId.length <= 128
|
|
107
|
+
? {
|
|
108
|
+
recipientInstanceId,
|
|
109
|
+
recipientRegistrationGeneration,
|
|
110
|
+
recipientSessionId,
|
|
111
|
+
recipientBindingKey,
|
|
112
|
+
operationId,
|
|
113
|
+
}
|
|
114
|
+
: undefined;
|
|
115
|
+
}
|
|
116
|
+
/** Non-empty, duplicate-free, non-negative safe-integer Telegram update ids. */
|
|
117
|
+
function isUniqueSourceUpdateIds(value) {
|
|
118
|
+
return (Array.isArray(value) &&
|
|
119
|
+
value.length > 0 &&
|
|
120
|
+
value.every((id) => Number.isSafeInteger(id) && id >= 0) &&
|
|
121
|
+
new Set(value).size === value.length);
|
|
122
|
+
}
|
|
123
|
+
function parsePreparedCommandSource(value) {
|
|
124
|
+
if (!isRecord(value) ||
|
|
125
|
+
!hasOnlyWireKeys(value, [
|
|
126
|
+
"journalBindingKey",
|
|
127
|
+
"updateId",
|
|
128
|
+
"sourceSha256",
|
|
129
|
+
]) ||
|
|
130
|
+
typeof value.journalBindingKey !== "string" ||
|
|
131
|
+
!value.journalBindingKey ||
|
|
132
|
+
!Number.isSafeInteger(value.updateId) ||
|
|
133
|
+
value.updateId < 0 ||
|
|
134
|
+
typeof value.sourceSha256 !== "string" ||
|
|
135
|
+
!/^[a-f0-9]{64}$/u.test(value.sourceSha256))
|
|
136
|
+
return undefined;
|
|
137
|
+
return {
|
|
138
|
+
journalBindingKey: value.journalBindingKey,
|
|
139
|
+
updateId: value.updateId,
|
|
140
|
+
sourceSha256: value.sourceSha256,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
133
143
|
export function createTelegramBusProtocolIdentity(input) {
|
|
134
144
|
const runtimeBuild = input.runtimeBuild.trim();
|
|
135
145
|
if (!runtimeBuild || runtimeBuild.length > 128) {
|
|
@@ -137,8 +147,7 @@ export function createTelegramBusProtocolIdentity(input) {
|
|
|
137
147
|
}
|
|
138
148
|
const capabilities = [...new Set(input.capabilities ?? [])].sort();
|
|
139
149
|
if (capabilities.length > 32 ||
|
|
140
|
-
capabilities.some((capability) => capability.length > 128 ||
|
|
141
|
-
!/^[a-z0-9][a-z0-9._-]*$/u.test(capability))) {
|
|
150
|
+
capabilities.some((capability) => capability.length > 128 || !/^[a-z0-9][a-z0-9._-]*$/u.test(capability))) {
|
|
142
151
|
throw new Error("Telegram bus capabilities must be canonical identifiers.");
|
|
143
152
|
}
|
|
144
153
|
return {
|
|
@@ -161,13 +170,20 @@ export function hasTelegramBusCapability(identity, capability) {
|
|
|
161
170
|
return identity?.capabilities.includes(capability) ?? false;
|
|
162
171
|
}
|
|
163
172
|
export function getTelegramInputCustodyPeerReadiness(followers) {
|
|
164
|
-
return followers.map(follower => {
|
|
173
|
+
return followers.map((follower) => {
|
|
165
174
|
if (!follower.registrationGeneration || !follower.protocol)
|
|
166
175
|
return "unknown";
|
|
167
176
|
return hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION) &&
|
|
168
|
-
hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE)
|
|
177
|
+
hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE)
|
|
178
|
+
? "ready"
|
|
179
|
+
: "legacy";
|
|
169
180
|
});
|
|
170
181
|
}
|
|
182
|
+
/** Both peers advertise every required capability over a compatible protocol; reads only the two identities. */
|
|
183
|
+
export function hasTelegramBusSharedCapabilities(local, remote, required) {
|
|
184
|
+
return (required.every((capability) => hasTelegramBusCapability(local, capability) &&
|
|
185
|
+
hasTelegramBusCapability(remote, capability)) && getTelegramBusProtocolCompatibility({ local, remote }).compatible);
|
|
186
|
+
}
|
|
171
187
|
export function getTelegramBusProtocolCompatibility(input) {
|
|
172
188
|
if (!input.remote) {
|
|
173
189
|
return {
|
|
@@ -196,7 +212,12 @@ export function getTelegramBusProtocolCompatibility(input) {
|
|
|
196
212
|
: { compatible: true, missingCapabilities: [] };
|
|
197
213
|
}
|
|
198
214
|
export function getTelegramBusSocketPath(agentDir = resolveAgentDir(), platform = getPlatform(), profileName, layout) {
|
|
199
|
-
return getTelegramBusLeaderEndpoint({
|
|
215
|
+
return getTelegramBusLeaderEndpoint({
|
|
216
|
+
agentDir,
|
|
217
|
+
platform,
|
|
218
|
+
profileName,
|
|
219
|
+
layout,
|
|
220
|
+
});
|
|
200
221
|
}
|
|
201
222
|
export function getTelegramBusFollowerSocketPath(instanceId, agentDir = resolveAgentDir(), platform = getPlatform(), profileName, layout) {
|
|
202
223
|
return getTelegramBusFollowerEndpoint({
|
|
@@ -207,6 +228,20 @@ export function getTelegramBusFollowerSocketPath(instanceId, agentDir = resolveA
|
|
|
207
228
|
layout,
|
|
208
229
|
});
|
|
209
230
|
}
|
|
231
|
+
/** Whether a live follower is still exactly the captured registration: identity, process, session, endpoint and negotiated protocol. */
|
|
232
|
+
export function isSameTelegramBusFollowerRegistration(live, captured) {
|
|
233
|
+
return (live.instanceId === captured.instanceId &&
|
|
234
|
+
live.profileKey === captured.profileKey &&
|
|
235
|
+
live.sessionId === captured.sessionId &&
|
|
236
|
+
live.registrationGeneration === captured.registrationGeneration &&
|
|
237
|
+
live.pid === captured.pid &&
|
|
238
|
+
live.processBirthId === captured.processBirthId &&
|
|
239
|
+
live.sessionGeneration === captured.sessionGeneration &&
|
|
240
|
+
live.cwd === captured.cwd &&
|
|
241
|
+
live.slot === captured.slot &&
|
|
242
|
+
live.busSocketPath === captured.busSocketPath &&
|
|
243
|
+
isDeepStrictEqual(live.protocol, captured.protocol));
|
|
244
|
+
}
|
|
210
245
|
export function getTelegramFollowerTargetOwnership(input) {
|
|
211
246
|
const liveFollower = input.followers.find((follower) => {
|
|
212
247
|
return (follower.target?.chatId === input.target.chatId &&
|
|
@@ -228,30 +263,17 @@ export function getTelegramFollowerTargetOwnership(input) {
|
|
|
228
263
|
}
|
|
229
264
|
const TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD = "__piTelegramAggregateDelivery";
|
|
230
265
|
const TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD = "__piTelegramCrossTargetDelivery";
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
...body,
|
|
234
|
-
[TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD]: true,
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
function isTelegramBusAggregateDelivery(body) {
|
|
266
|
+
function hasTelegramBusDeliveryMarker(body, marker) {
|
|
238
267
|
return Boolean(body &&
|
|
239
268
|
typeof body === "object" &&
|
|
240
269
|
!Array.isArray(body) &&
|
|
241
|
-
body[
|
|
242
|
-
true);
|
|
270
|
+
body[marker] === true);
|
|
243
271
|
}
|
|
244
|
-
export function
|
|
245
|
-
return {
|
|
246
|
-
...body,
|
|
247
|
-
[TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD]: true,
|
|
248
|
-
};
|
|
272
|
+
export function markTelegramBusAggregateDelivery(body) {
|
|
273
|
+
return { ...body, [TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD]: true };
|
|
249
274
|
}
|
|
250
|
-
function
|
|
251
|
-
return
|
|
252
|
-
typeof body === "object" &&
|
|
253
|
-
!Array.isArray(body) &&
|
|
254
|
-
body[TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD] === true);
|
|
275
|
+
export function markTelegramBusCrossTargetDelivery(body) {
|
|
276
|
+
return { ...body, [TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD]: true };
|
|
255
277
|
}
|
|
256
278
|
export function stripTelegramBusApiMetadata(body) {
|
|
257
279
|
if (!(TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD in body) &&
|
|
@@ -317,8 +339,7 @@ export function isTelegramFollowerApiCallAllowed(input) {
|
|
|
317
339
|
return target.threadId !== undefined;
|
|
318
340
|
const parsedThreadId = typeof threadId === "number" ? threadId : Number(threadId);
|
|
319
341
|
return (Number.isInteger(parsedThreadId) &&
|
|
320
|
-
(target.threadId === undefined ||
|
|
321
|
-
!matchesId(threadId, target.threadId)));
|
|
342
|
+
(target.threadId === undefined || !matchesId(threadId, target.threadId)));
|
|
322
343
|
};
|
|
323
344
|
const isTargetMessageScoped = (body) => {
|
|
324
345
|
if (!isTargetChatScoped(body))
|
|
@@ -355,12 +376,12 @@ export function isTelegramFollowerApiCallAllowed(input) {
|
|
|
355
376
|
if (apiMethod === "sendChatAction")
|
|
356
377
|
return isTargetChatScoped(input.args[1]);
|
|
357
378
|
if (apiMethod === "sendMessage" &&
|
|
358
|
-
|
|
379
|
+
hasTelegramBusDeliveryMarker(input.args[1], TELEGRAM_BUS_AGGREGATE_DELIVERY_FIELD)) {
|
|
359
380
|
const body = input.args[1];
|
|
360
381
|
return body.message_thread_id === undefined && isTargetChatScoped(body);
|
|
361
382
|
}
|
|
362
383
|
if ((apiMethod === "sendMessage" || apiMethod === "sendRichMessage") &&
|
|
363
|
-
|
|
384
|
+
hasTelegramBusDeliveryMarker(input.args[1], TELEGRAM_BUS_CROSS_TARGET_DELIVERY_FIELD)) {
|
|
364
385
|
return isDifferentTargetScoped(input.args[1]);
|
|
365
386
|
}
|
|
366
387
|
if (apiMethod === "deleteMessage" ||
|
|
@@ -401,8 +422,10 @@ export function createTelegramBusFollowerDeliveryIdentity(input) {
|
|
|
401
422
|
!Number.isSafeInteger(input.sourceUpdateId) ||
|
|
402
423
|
input.sourceUpdateId < 0 ||
|
|
403
424
|
(input.sourceClaim !== undefined &&
|
|
404
|
-
(!input.sourceClaim.acquisitionId ||
|
|
405
|
-
|
|
425
|
+
(!input.sourceClaim.acquisitionId ||
|
|
426
|
+
input.sourceClaim.acquisitionId.length > 256 ||
|
|
427
|
+
!input.sourceClaim.handoffId ||
|
|
428
|
+
input.sourceClaim.handoffId.length > 256))) {
|
|
406
429
|
throw new Error("Telegram follower delivery identity is incomplete.");
|
|
407
430
|
}
|
|
408
431
|
const deliveryId = createHash("sha256")
|
|
@@ -417,23 +440,29 @@ export function createTelegramBusFollowerDeliveryIdentity(input) {
|
|
|
417
440
|
deliveryId: `telegram-follower-v1-${deliveryId}`,
|
|
418
441
|
sourceUpdateId: input.sourceUpdateId,
|
|
419
442
|
recipientBindingKey: input.recipientBindingKey,
|
|
420
|
-
...(input.sourceRecoveryKey
|
|
443
|
+
...(input.sourceRecoveryKey
|
|
444
|
+
? { sourceRecoveryKey: input.sourceRecoveryKey }
|
|
445
|
+
: {}),
|
|
421
446
|
...(input.sourceClaim ? { sourceClaim: { ...input.sourceClaim } } : {}),
|
|
422
447
|
};
|
|
423
448
|
}
|
|
424
449
|
export function canUseTelegramBusInputCustodyReference(input) {
|
|
425
|
-
return hasTelegramBusCapability(input.local, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE) &&
|
|
426
|
-
hasTelegramBusCapability(input.remote, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE);
|
|
450
|
+
return (hasTelegramBusCapability(input.local, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE) &&
|
|
451
|
+
hasTelegramBusCapability(input.remote, TELEGRAM_BUS_CAPABILITY_INPUT_CUSTODY_REFERENCE));
|
|
427
452
|
}
|
|
428
453
|
export function createTelegramBusFollowerSourceReferenceDeliveryIdentity(input) {
|
|
429
454
|
if (!input.sourceRecoveryKey || !input.source.owner.handoffId)
|
|
430
455
|
throw new Error("Telegram follower source-reference delivery requires an accepted handoff.");
|
|
431
|
-
return createTelegramBusFollowerDeliveryIdentity({
|
|
456
|
+
return createTelegramBusFollowerDeliveryIdentity({
|
|
457
|
+
kind: input.kind,
|
|
432
458
|
recipientBindingKey: input.recipientBindingKey,
|
|
433
459
|
sourceUpdateId: input.source.updateId,
|
|
434
460
|
sourceRecoveryKey: input.sourceRecoveryKey,
|
|
435
|
-
sourceClaim: {
|
|
436
|
-
|
|
461
|
+
sourceClaim: {
|
|
462
|
+
acquisitionId: input.source.owner.acquisitionId,
|
|
463
|
+
handoffId: input.source.owner.handoffId,
|
|
464
|
+
},
|
|
465
|
+
});
|
|
437
466
|
}
|
|
438
467
|
export function getTelegramBusEnvelopeTrafficClass(envelope) {
|
|
439
468
|
if (envelope.kind === "follower.register" ||
|
|
@@ -478,18 +507,23 @@ export function parseTelegramBusEnvelope(line) {
|
|
|
478
507
|
envelope = parseRegisterEnvelope(value, requestId, kind);
|
|
479
508
|
break;
|
|
480
509
|
case "follower.heartbeat":
|
|
481
|
-
envelope = parseHeartbeatEnvelope(value, requestId);
|
|
482
|
-
break;
|
|
483
510
|
case "follower.disconnect":
|
|
484
|
-
envelope =
|
|
511
|
+
envelope = parseFollowerPresenceEnvelope(value, requestId, kind);
|
|
485
512
|
break;
|
|
486
513
|
case "follower.setThreadDisplayMode":
|
|
487
514
|
if (typeof value.instanceId === "string" &&
|
|
488
515
|
typeof value.registrationGeneration === "string" &&
|
|
489
|
-
(value.mode === "letters" ||
|
|
490
|
-
value.mode === "
|
|
491
|
-
|
|
492
|
-
|
|
516
|
+
(value.mode === "letters" ||
|
|
517
|
+
value.mode === "names" ||
|
|
518
|
+
value.mode === "directory-snake" ||
|
|
519
|
+
value.mode === "directory-title")) {
|
|
520
|
+
envelope = {
|
|
521
|
+
kind,
|
|
522
|
+
requestId,
|
|
523
|
+
instanceId: value.instanceId,
|
|
524
|
+
registrationGeneration: value.registrationGeneration,
|
|
525
|
+
mode: value.mode,
|
|
526
|
+
};
|
|
493
527
|
}
|
|
494
528
|
break;
|
|
495
529
|
case "follower.renameThread":
|
|
@@ -517,7 +551,8 @@ export function parseTelegramBusEnvelope(line) {
|
|
|
517
551
|
const intent = normalizeTelegramSessionReplacementIntent(value.intent);
|
|
518
552
|
if (typeof value.instanceId === "string" &&
|
|
519
553
|
typeof value.registrationGeneration === "string" &&
|
|
520
|
-
typeof value.sentAtMs === "number" &&
|
|
554
|
+
typeof value.sentAtMs === "number" &&
|
|
555
|
+
intent) {
|
|
521
556
|
envelope = {
|
|
522
557
|
kind,
|
|
523
558
|
requestId,
|
|
@@ -547,15 +582,122 @@ export function parseTelegramBusEnvelope(line) {
|
|
|
547
582
|
case "leader.forwardEditedMessage":
|
|
548
583
|
envelope = parseForwardMessageEnvelope(value, requestId, "leader.forwardEditedMessage");
|
|
549
584
|
break;
|
|
585
|
+
case "leader.prepareLiveRebind": {
|
|
586
|
+
const selectedCommand = parseSelectedCommandInput(value.selectedCommand);
|
|
587
|
+
if (!hasOnlyWireKeys(value, [
|
|
588
|
+
"kind",
|
|
589
|
+
"requestId",
|
|
590
|
+
"auth",
|
|
591
|
+
"recipientInstanceId",
|
|
592
|
+
"recipientRegistrationGeneration",
|
|
593
|
+
"recipientSessionId",
|
|
594
|
+
"recipientBindingKey",
|
|
595
|
+
"operationId",
|
|
596
|
+
"updates",
|
|
597
|
+
"selectedCommand",
|
|
598
|
+
"sentAtMs",
|
|
599
|
+
]) ||
|
|
600
|
+
(Object.hasOwn(value, "selectedCommand") &&
|
|
601
|
+
(!selectedCommand ||
|
|
602
|
+
!Array.isArray(value.updates) ||
|
|
603
|
+
value.updates.length !== 1)))
|
|
604
|
+
break;
|
|
605
|
+
const recipient = parseLiveRebindRecipient(value);
|
|
606
|
+
if (recipient &&
|
|
607
|
+
Array.isArray(value.updates) &&
|
|
608
|
+
value.updates.every(isRecord) &&
|
|
609
|
+
isUniqueSourceUpdateIds(value.updates.map((update) => update.update_id)) &&
|
|
610
|
+
typeof value.sentAtMs === "number" &&
|
|
611
|
+
Number.isFinite(value.sentAtMs)) {
|
|
612
|
+
envelope = {
|
|
613
|
+
kind: "leader.prepareLiveRebind",
|
|
614
|
+
requestId,
|
|
615
|
+
...recipient,
|
|
616
|
+
updates: value.updates,
|
|
617
|
+
sentAtMs: value.sentAtMs,
|
|
618
|
+
...(selectedCommand ? { selectedCommand } : {}),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
case "leader.applyLiveRebind":
|
|
624
|
+
case "leader.settleLiveRebind": {
|
|
625
|
+
const selectedCommand = parseSelectedCommandInput(value.selectedCommand), preparedSource = parsePreparedCommandSource(value.preparedSource);
|
|
626
|
+
if (!hasOnlyWireKeys(value, [
|
|
627
|
+
"kind",
|
|
628
|
+
"requestId",
|
|
629
|
+
"auth",
|
|
630
|
+
"recipientInstanceId",
|
|
631
|
+
"recipientRegistrationGeneration",
|
|
632
|
+
"recipientSessionId",
|
|
633
|
+
"recipientBindingKey",
|
|
634
|
+
"operationId",
|
|
635
|
+
"sourceUpdateIds",
|
|
636
|
+
"mode",
|
|
637
|
+
"oldTarget",
|
|
638
|
+
"selectedCommand",
|
|
639
|
+
"preparedSource",
|
|
640
|
+
"sentAtMs",
|
|
641
|
+
]) ||
|
|
642
|
+
(Object.hasOwn(value, "preparedSource") && !selectedCommand) ||
|
|
643
|
+
(value.mode === "observe-command" &&
|
|
644
|
+
(!selectedCommand || Object.hasOwn(value, "oldTarget"))))
|
|
645
|
+
break;
|
|
646
|
+
if (Object.hasOwn(value, "selectedCommand") &&
|
|
647
|
+
(!selectedCommand ||
|
|
648
|
+
!Array.isArray(value.sourceUpdateIds) ||
|
|
649
|
+
value.sourceUpdateIds.length !== 1 ||
|
|
650
|
+
!preparedSource ||
|
|
651
|
+
preparedSource.updateId !== value.sourceUpdateIds[0] ||
|
|
652
|
+
preparedSource.journalBindingKey !== value.recipientBindingKey))
|
|
653
|
+
break;
|
|
654
|
+
const recipient = parseLiveRebindRecipient(value);
|
|
655
|
+
if (recipient &&
|
|
656
|
+
(value.kind === "leader.applyLiveRebind"
|
|
657
|
+
? value.mode === "apply" || value.mode === "inspect"
|
|
658
|
+
: value.mode === "release" ||
|
|
659
|
+
value.mode === "discard" ||
|
|
660
|
+
value.mode === "observe-command" ||
|
|
661
|
+
(value.mode === "observe" &&
|
|
662
|
+
!!parseThreadTarget(value.oldTarget))) &&
|
|
663
|
+
isUniqueSourceUpdateIds(value.sourceUpdateIds) &&
|
|
664
|
+
typeof value.sentAtMs === "number" &&
|
|
665
|
+
Number.isFinite(value.sentAtMs)) {
|
|
666
|
+
envelope = {
|
|
667
|
+
kind: value.kind,
|
|
668
|
+
requestId,
|
|
669
|
+
...recipient,
|
|
670
|
+
sourceUpdateIds: value.sourceUpdateIds,
|
|
671
|
+
mode: value.mode,
|
|
672
|
+
sentAtMs: value.sentAtMs,
|
|
673
|
+
...(value.mode === "observe"
|
|
674
|
+
? { oldTarget: parseThreadTarget(value.oldTarget) }
|
|
675
|
+
: {}),
|
|
676
|
+
...(selectedCommand ? { selectedCommand, preparedSource } : {}),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
550
681
|
case "leader.workspaceRestore":
|
|
551
|
-
if (typeof value.recipientInstanceId === "string" &&
|
|
552
|
-
|
|
553
|
-
typeof value.
|
|
682
|
+
if (typeof value.recipientInstanceId === "string" &&
|
|
683
|
+
value.recipientInstanceId &&
|
|
684
|
+
typeof value.recipientRegistrationGeneration === "string" &&
|
|
685
|
+
value.recipientRegistrationGeneration &&
|
|
686
|
+
typeof value.operationId === "string" &&
|
|
687
|
+
value.operationId.length > 0 &&
|
|
688
|
+
value.operationId.length <= 128 &&
|
|
554
689
|
(value.mode === "apply" || value.mode === "inspect") &&
|
|
555
|
-
typeof value.sentAtMs === "number" &&
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
690
|
+
typeof value.sentAtMs === "number" &&
|
|
691
|
+
Number.isFinite(value.sentAtMs)) {
|
|
692
|
+
envelope = {
|
|
693
|
+
kind: "leader.workspaceRestore",
|
|
694
|
+
requestId,
|
|
695
|
+
recipientInstanceId: value.recipientInstanceId,
|
|
696
|
+
recipientRegistrationGeneration: value.recipientRegistrationGeneration,
|
|
697
|
+
operationId: value.operationId,
|
|
698
|
+
mode: value.mode,
|
|
699
|
+
sentAtMs: value.sentAtMs,
|
|
700
|
+
};
|
|
559
701
|
}
|
|
560
702
|
break;
|
|
561
703
|
case "leader.offerQueueHandoff":
|
|
@@ -570,6 +712,9 @@ export function parseTelegramBusEnvelope(line) {
|
|
|
570
712
|
case "follower.routeAgentMessage":
|
|
571
713
|
envelope = parseRouteAgentMessageEnvelope(value, requestId);
|
|
572
714
|
break;
|
|
715
|
+
case "follower.deliverSelectedMenu":
|
|
716
|
+
envelope = parseSelectedMenuDeliveryEnvelope(value, requestId);
|
|
717
|
+
break;
|
|
573
718
|
case "follower.callApi":
|
|
574
719
|
envelope = parseCallApiEnvelope(value, requestId);
|
|
575
720
|
break;
|
|
@@ -614,11 +759,24 @@ export function resolveTelegramBusSocketPath(source, platform = getPlatform()) {
|
|
|
614
759
|
.slice(0, 16);
|
|
615
760
|
return join(fallbackDir, `${digest}.sock`);
|
|
616
761
|
}
|
|
762
|
+
/** Local IPC issuance proof only: this callback/error never enters the envelope or certifies a remote API effect. */
|
|
763
|
+
export class TelegramBusLocalAuthorityError extends Error {
|
|
764
|
+
requestIssued;
|
|
765
|
+
constructor(requestIssued, reason) {
|
|
766
|
+
super(reason
|
|
767
|
+
? `Telegram bus local authority is unavailable: ${reason}`
|
|
768
|
+
: "Telegram bus local authority is unavailable.");
|
|
769
|
+
this.name = "TelegramBusLocalAuthorityError";
|
|
770
|
+
this.requestIssued = requestIssued;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
617
773
|
export function isTelegramBusForwardOwnershipCurrent(expected, current) {
|
|
618
|
-
return Boolean(current &&
|
|
774
|
+
return Boolean(current &&
|
|
775
|
+
current.instanceId === expected.instanceId &&
|
|
619
776
|
current.ownerGeneration === expected.ownerGeneration &&
|
|
620
777
|
current.recipientBindingKey === expected.recipientBindingKey &&
|
|
621
|
-
JSON.stringify(current.protocolIdentity) ===
|
|
778
|
+
JSON.stringify(current.protocolIdentity) ===
|
|
779
|
+
JSON.stringify(expected.protocolIdentity));
|
|
622
780
|
}
|
|
623
781
|
function getTelegramBusForwardSourceUpdateId(value) {
|
|
624
782
|
if (!isRecord(value))
|
|
@@ -635,6 +793,11 @@ function createTelegramBusForwardDelivery(kind, sourceUpdateId, recipientBinding
|
|
|
635
793
|
sourceUpdateId,
|
|
636
794
|
});
|
|
637
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* Leader-to-follower forwards and control replies share the stale-liveness window: a follower doing journal work or stalled
|
|
798
|
+
* by an ordinary multi-second Pi/TUI turn, or a slower Windows named pipe, is not a lost recipient.
|
|
799
|
+
*/
|
|
800
|
+
const TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS = TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
|
|
638
801
|
export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
|
|
639
802
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
640
803
|
const reject = (input) => {
|
|
@@ -684,21 +847,32 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
|
|
|
684
847
|
};
|
|
685
848
|
}
|
|
686
849
|
const sourceReference = canUseTelegramBusInputCustodyReference({
|
|
687
|
-
local: deps.localProtocolIdentity,
|
|
850
|
+
local: deps.localProtocolIdentity,
|
|
851
|
+
remote: ownership.protocolIdentity,
|
|
688
852
|
});
|
|
689
|
-
const reference = sourceReference
|
|
690
|
-
|
|
691
|
-
|
|
853
|
+
const reference = sourceReference
|
|
854
|
+
? deps.resolveInputCustodyReference?.({
|
|
855
|
+
sourceUpdateId,
|
|
856
|
+
recipientBindingKey: ownership.recipientBindingKey,
|
|
857
|
+
})
|
|
858
|
+
: undefined;
|
|
692
859
|
if (sourceReference && !reference)
|
|
693
|
-
return {
|
|
694
|
-
|
|
860
|
+
return {
|
|
861
|
+
settlement: reject({
|
|
862
|
+
status: "retryable",
|
|
863
|
+
failureClass: "source-reference-missing",
|
|
695
864
|
message: "Forwarded Telegram update has no exact custody reference.",
|
|
696
|
-
envelopeKind: kind,
|
|
697
|
-
|
|
865
|
+
envelopeKind: kind,
|
|
866
|
+
ownership,
|
|
867
|
+
sourceUpdateId,
|
|
868
|
+
}),
|
|
869
|
+
};
|
|
698
870
|
const delivery = reference
|
|
699
871
|
? createTelegramBusFollowerSourceReferenceDeliveryIdentity({
|
|
700
|
-
kind: "leader.wakeInputCustody",
|
|
701
|
-
|
|
872
|
+
kind: "leader.wakeInputCustody",
|
|
873
|
+
recipientBindingKey: ownership.recipientBindingKey,
|
|
874
|
+
sourceRecoveryKey: reference.sourceRecoveryKey,
|
|
875
|
+
source: reference.source,
|
|
702
876
|
})
|
|
703
877
|
: createTelegramBusForwardDelivery(kind, sourceUpdateId, ownership.recipientBindingKey);
|
|
704
878
|
if (!ownership.ownerGeneration) {
|
|
@@ -720,11 +894,15 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
|
|
|
720
894
|
};
|
|
721
895
|
};
|
|
722
896
|
const send = async (envelope, ownership) => {
|
|
723
|
-
if (deps.validateForwardOwnership &&
|
|
897
|
+
if (deps.validateForwardOwnership &&
|
|
898
|
+
!deps.validateForwardOwnership(ownership))
|
|
724
899
|
return reject({
|
|
725
|
-
status: "retryable",
|
|
900
|
+
status: "retryable",
|
|
901
|
+
failureClass: "recipient-ownership-stale",
|
|
726
902
|
message: "Telegram follower ownership changed before forwarding.",
|
|
727
|
-
envelopeKind: envelope.kind,
|
|
903
|
+
envelopeKind: envelope.kind,
|
|
904
|
+
ownership,
|
|
905
|
+
delivery: envelope.delivery,
|
|
728
906
|
});
|
|
729
907
|
if (deps.getAuthSecret)
|
|
730
908
|
envelope.auth = deps.getAuthSecret();
|
|
@@ -734,7 +912,7 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
|
|
|
734
912
|
response = await sendTelegramBusLocalEnvelope({
|
|
735
913
|
socketPath,
|
|
736
914
|
envelope,
|
|
737
|
-
timeoutMs: deps.timeoutMs,
|
|
915
|
+
timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
|
|
738
916
|
retry: getTelegramBusTransportRetryPolicy({
|
|
739
917
|
endpoint: socketPath,
|
|
740
918
|
operation: "operation",
|
|
@@ -809,88 +987,52 @@ export function createTelegramBusForeignOwnedUpdateForwarder(deps) {
|
|
|
809
987
|
}
|
|
810
988
|
return { status: "accepted", delivery: envelope.delivery };
|
|
811
989
|
};
|
|
990
|
+
// A recipient already holding the source reference is only woken; it never receives a second payload copy.
|
|
991
|
+
const forward = (kind, value, ownership, build) => {
|
|
992
|
+
const prepared = prepare(kind, value, ownership);
|
|
993
|
+
if ("settlement" in prepared)
|
|
994
|
+
return Promise.resolve(prepared.settlement);
|
|
995
|
+
const base = {
|
|
996
|
+
requestId: deps.createRequestId(),
|
|
997
|
+
recipientInstanceId: ownership.instanceId,
|
|
998
|
+
recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
|
|
999
|
+
delivery: prepared.delivery,
|
|
1000
|
+
};
|
|
1001
|
+
return send(prepared.sourceReference
|
|
1002
|
+
? { kind: "leader.wakeInputCustody", ...base, sentAtMs: getNowMs() }
|
|
1003
|
+
: build(base, getNowMs()), ownership);
|
|
1004
|
+
};
|
|
812
1005
|
return {
|
|
813
|
-
forwardCallback: ({ query, ownership }) => {
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
query,
|
|
829
|
-
sentAtMs: getNowMs(),
|
|
830
|
-
}, ownership);
|
|
831
|
-
},
|
|
832
|
-
forwardReaction: ({ reactionUpdate, ownership }) => {
|
|
833
|
-
const prepared = prepare("leader.forwardReaction", reactionUpdate, ownership);
|
|
834
|
-
if ("settlement" in prepared)
|
|
835
|
-
return Promise.resolve(prepared.settlement);
|
|
836
|
-
if (prepared.sourceReference)
|
|
837
|
-
return send({ kind: "leader.wakeInputCustody",
|
|
838
|
-
requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
|
|
839
|
-
recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
|
|
840
|
-
delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
|
|
841
|
-
return send({
|
|
842
|
-
kind: "leader.forwardReaction",
|
|
843
|
-
requestId: deps.createRequestId(),
|
|
844
|
-
recipientInstanceId: ownership.instanceId,
|
|
845
|
-
recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
|
|
846
|
-
delivery: prepared.delivery,
|
|
847
|
-
reactionUpdate,
|
|
848
|
-
sentAtMs: getNowMs(),
|
|
849
|
-
}, ownership);
|
|
850
|
-
},
|
|
851
|
-
forwardMessage: ({ message, ownership }) => {
|
|
852
|
-
const prepared = prepare("leader.forwardMessage", message, ownership);
|
|
853
|
-
if ("settlement" in prepared)
|
|
854
|
-
return Promise.resolve(prepared.settlement);
|
|
855
|
-
if (prepared.sourceReference)
|
|
856
|
-
return send({ kind: "leader.wakeInputCustody",
|
|
857
|
-
requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
|
|
858
|
-
recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
|
|
859
|
-
delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
|
|
860
|
-
return send({
|
|
1006
|
+
forwardCallback: ({ query, ownership }) => forward("leader.forwardCallback", query, ownership, (base, sentAtMs) => ({
|
|
1007
|
+
kind: "leader.forwardCallback",
|
|
1008
|
+
...base,
|
|
1009
|
+
query,
|
|
1010
|
+
sentAtMs,
|
|
1011
|
+
})),
|
|
1012
|
+
forwardReaction: ({ reactionUpdate, ownership }) => forward("leader.forwardReaction", reactionUpdate, ownership, (base, sentAtMs) => ({
|
|
1013
|
+
kind: "leader.forwardReaction",
|
|
1014
|
+
...base,
|
|
1015
|
+
reactionUpdate,
|
|
1016
|
+
sentAtMs,
|
|
1017
|
+
})),
|
|
1018
|
+
forwardMessage: ({ message, ownership }) => forward("leader.forwardMessage", message, ownership, (base, sentAtMs) => {
|
|
1019
|
+
const forwardCommentBatchPosition = deps.getForwardCommentBatchPosition?.(message);
|
|
1020
|
+
return {
|
|
861
1021
|
kind: "leader.forwardMessage",
|
|
862
|
-
|
|
863
|
-
recipientInstanceId: ownership.instanceId,
|
|
864
|
-
recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
|
|
865
|
-
delivery: prepared.delivery,
|
|
1022
|
+
...base,
|
|
866
1023
|
message,
|
|
867
|
-
...(
|
|
868
|
-
? {
|
|
869
|
-
forwardCommentBatchPosition: deps.getForwardCommentBatchPosition(message),
|
|
870
|
-
}
|
|
1024
|
+
...(forwardCommentBatchPosition !== undefined
|
|
1025
|
+
? { forwardCommentBatchPosition }
|
|
871
1026
|
: {}),
|
|
872
|
-
sentAtMs
|
|
873
|
-
}
|
|
874
|
-
},
|
|
875
|
-
forwardEditedMessage: ({ message, ownership }) => {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
requestId: deps.createRequestId(), recipientInstanceId: ownership.instanceId,
|
|
882
|
-
recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
|
|
883
|
-
delivery: prepared.delivery, sentAtMs: getNowMs() }, ownership);
|
|
884
|
-
return send({
|
|
885
|
-
kind: "leader.forwardEditedMessage",
|
|
886
|
-
requestId: deps.createRequestId(),
|
|
887
|
-
recipientInstanceId: ownership.instanceId,
|
|
888
|
-
recipientRegistrationGeneration: prepared.recipientRegistrationGeneration,
|
|
889
|
-
delivery: prepared.delivery,
|
|
890
|
-
message,
|
|
891
|
-
sentAtMs: getNowMs(),
|
|
892
|
-
}, ownership);
|
|
893
|
-
},
|
|
1027
|
+
sentAtMs,
|
|
1028
|
+
};
|
|
1029
|
+
}),
|
|
1030
|
+
forwardEditedMessage: ({ message, ownership }) => forward("leader.forwardEditedMessage", message, ownership, (base, sentAtMs) => ({
|
|
1031
|
+
kind: "leader.forwardEditedMessage",
|
|
1032
|
+
...base,
|
|
1033
|
+
message,
|
|
1034
|
+
sentAtMs,
|
|
1035
|
+
})),
|
|
894
1036
|
};
|
|
895
1037
|
}
|
|
896
1038
|
export function listTelegramBusLiveThreadTargets(input) {
|
|
@@ -912,48 +1054,441 @@ export function createTelegramBusWorkspaceRestoreController(deps) {
|
|
|
912
1054
|
return undefined;
|
|
913
1055
|
const follower = deps.getFollower(input.instanceId);
|
|
914
1056
|
const secret = deps.getAuthSecret();
|
|
915
|
-
if (!secret ||
|
|
916
|
-
follower
|
|
917
|
-
!
|
|
918
|
-
|
|
919
|
-
|
|
1057
|
+
if (!secret ||
|
|
1058
|
+
!follower?.registrationGeneration ||
|
|
1059
|
+
!follower.busSocketPath ||
|
|
1060
|
+
follower.sessionId !== input.sessionId ||
|
|
1061
|
+
follower.slot !== input.slot ||
|
|
1062
|
+
!hasTelegramBusSharedCapabilities(deps.localProtocolIdentity, follower.protocol, [TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE]))
|
|
920
1063
|
return undefined;
|
|
921
1064
|
const captured = structuredClone(follower);
|
|
922
|
-
const expected = {
|
|
923
|
-
|
|
1065
|
+
const expected = {
|
|
1066
|
+
operationId: input.operationId,
|
|
1067
|
+
sessionId: input.sessionId,
|
|
1068
|
+
slot: input.slot,
|
|
1069
|
+
target: { ...input.target },
|
|
1070
|
+
oldTarget: { ...input.oldTarget },
|
|
1071
|
+
mode: input.mode,
|
|
1072
|
+
};
|
|
924
1073
|
const current = () => {
|
|
925
|
-
if (!isCurrent() ||
|
|
1074
|
+
if (!isCurrent() ||
|
|
1075
|
+
deps.getAuthSecret() !== secret ||
|
|
926
1076
|
!hasTelegramBusCapability(deps.localProtocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE))
|
|
927
1077
|
return false;
|
|
928
1078
|
const live = deps.getFollower(captured.instanceId);
|
|
929
|
-
return !!live &&
|
|
930
|
-
live.
|
|
931
|
-
live.
|
|
1079
|
+
return (!!live &&
|
|
1080
|
+
live.registrationGeneration === captured.registrationGeneration &&
|
|
1081
|
+
live.sessionId === captured.sessionId &&
|
|
1082
|
+
live.cwd === captured.cwd &&
|
|
1083
|
+
live.slot === captured.slot &&
|
|
1084
|
+
live.busSocketPath === captured.busSocketPath &&
|
|
1085
|
+
live.target?.chatId === captured.target?.chatId &&
|
|
932
1086
|
live.target?.threadId === captured.target?.threadId &&
|
|
933
1087
|
hasTelegramBusCapability(live.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
|
|
934
|
-
getTelegramBusProtocolCompatibility({
|
|
1088
|
+
getTelegramBusProtocolCompatibility({
|
|
1089
|
+
local: deps.localProtocolIdentity,
|
|
1090
|
+
remote: live.protocol,
|
|
1091
|
+
}).compatible);
|
|
935
1092
|
};
|
|
936
1093
|
if (!current())
|
|
937
1094
|
return undefined;
|
|
938
1095
|
const requestId = deps.createRequestId();
|
|
939
|
-
const response = await sendTelegramBusLocalEnvelope({
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1096
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
1097
|
+
socketPath: captured.busSocketPath,
|
|
1098
|
+
timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
|
|
1099
|
+
retry: { attempts: 1, delayMs: 0 },
|
|
1100
|
+
envelope: {
|
|
1101
|
+
kind: "leader.workspaceRestore",
|
|
1102
|
+
requestId,
|
|
1103
|
+
recipientInstanceId: captured.instanceId,
|
|
1104
|
+
recipientRegistrationGeneration: captured.registrationGeneration,
|
|
1105
|
+
operationId: expected.operationId,
|
|
1106
|
+
mode: expected.mode,
|
|
1107
|
+
auth: secret,
|
|
1108
|
+
sentAtMs: Date.now(),
|
|
1109
|
+
},
|
|
1110
|
+
});
|
|
1111
|
+
if (!current() ||
|
|
1112
|
+
response?.kind !== "bus.ack" ||
|
|
1113
|
+
response.requestId !== requestId ||
|
|
1114
|
+
response.ok !== true ||
|
|
1115
|
+
!response.result ||
|
|
1116
|
+
typeof response.result !== "object")
|
|
945
1117
|
return undefined;
|
|
946
1118
|
const result = response.result;
|
|
947
1119
|
const recipient = result.recipient;
|
|
948
1120
|
const target = parseThreadTarget(result.target);
|
|
949
1121
|
const wanted = result.ready === true ? expected.target : expected.oldTarget;
|
|
950
|
-
if (result.operationId !== expected.operationId ||
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1122
|
+
if (result.operationId !== expected.operationId ||
|
|
1123
|
+
typeof result.ready !== "boolean" ||
|
|
1124
|
+
result.slot !== expected.slot ||
|
|
1125
|
+
!target ||
|
|
1126
|
+
target.chatId !== wanted.chatId ||
|
|
1127
|
+
target.threadId !== wanted.threadId ||
|
|
1128
|
+
recipient?.kind !== "follower" ||
|
|
1129
|
+
recipient.instanceId !== captured.instanceId ||
|
|
1130
|
+
recipient.sessionId !== expected.sessionId ||
|
|
1131
|
+
recipient.generation !== captured.registrationGeneration)
|
|
1132
|
+
return undefined;
|
|
1133
|
+
return {
|
|
1134
|
+
operationId: expected.operationId,
|
|
1135
|
+
recipient: {
|
|
1136
|
+
kind: "follower",
|
|
1137
|
+
instanceId: captured.instanceId,
|
|
1138
|
+
sessionId: expected.sessionId,
|
|
1139
|
+
generation: captured.registrationGeneration,
|
|
1140
|
+
},
|
|
1141
|
+
target,
|
|
1142
|
+
slot: expected.slot,
|
|
1143
|
+
ready: result.ready,
|
|
1144
|
+
};
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
/** Each capability has its own effect boundary; transport retries never replay save/dispatch/disposal. */
|
|
1148
|
+
export function createTelegramBusLiveRebindController(deps) {
|
|
1149
|
+
return async (input) => {
|
|
1150
|
+
const isCurrent = input.isCurrent.bind(input), localProtocol = structuredClone(deps.localProtocolIdentity);
|
|
1151
|
+
const selectedCommand = parseSelectedCommandInput(input.selectedCommand), held = selectedCommand, preparedSource = parsePreparedCommandSource(input.preparedSource);
|
|
1152
|
+
if (!hasOnlyWireKeys(input, [
|
|
1153
|
+
"operationId",
|
|
1154
|
+
"instanceId",
|
|
1155
|
+
"sessionId",
|
|
1156
|
+
"recipientBindingKey",
|
|
1157
|
+
"isCurrent",
|
|
1158
|
+
"selectedCommand",
|
|
1159
|
+
"preparedSource",
|
|
1160
|
+
"updates",
|
|
1161
|
+
"mode",
|
|
1162
|
+
"sourceUpdateIds",
|
|
1163
|
+
"slot",
|
|
1164
|
+
"target",
|
|
1165
|
+
"oldTarget",
|
|
1166
|
+
]) ||
|
|
1167
|
+
("mode" in input &&
|
|
1168
|
+
(![
|
|
1169
|
+
"apply",
|
|
1170
|
+
"inspect",
|
|
1171
|
+
"release",
|
|
1172
|
+
"discard",
|
|
1173
|
+
"observe",
|
|
1174
|
+
"observe-command",
|
|
1175
|
+
].includes(input.mode) ||
|
|
1176
|
+
(input.mode === "observe-command" && !selectedCommand))))
|
|
1177
|
+
return undefined;
|
|
1178
|
+
if ((input.preparedSource !== undefined &&
|
|
1179
|
+
(!held ||
|
|
1180
|
+
!preparedSource ||
|
|
1181
|
+
preparedSource.journalBindingKey !== input.recipientBindingKey ||
|
|
1182
|
+
preparedSource.updateId !==
|
|
1183
|
+
("mode" in input
|
|
1184
|
+
? input.sourceUpdateIds[0]
|
|
1185
|
+
: input.updates[0]?.update_id))) ||
|
|
1186
|
+
(held && "mode" in input && !preparedSource))
|
|
1187
|
+
return undefined;
|
|
1188
|
+
if (input.selectedCommand !== undefined &&
|
|
1189
|
+
(!held ||
|
|
1190
|
+
("mode" in input
|
|
1191
|
+
? input.sourceUpdateIds.length !== 1 ||
|
|
1192
|
+
!areTelegramTargetsEqual(input.target, held.target)
|
|
1193
|
+
: input.updates.length !== 1)))
|
|
1194
|
+
return undefined;
|
|
1195
|
+
const marker = selectedCommand ? { selectedCommand } : {};
|
|
1196
|
+
const expected = structuredClone({
|
|
1197
|
+
operationId: input.operationId,
|
|
1198
|
+
instanceId: input.instanceId,
|
|
1199
|
+
sessionId: input.sessionId,
|
|
1200
|
+
recipientBindingKey: input.recipientBindingKey,
|
|
1201
|
+
...(held
|
|
1202
|
+
? { ...marker, ...(preparedSource ? { preparedSource } : {}) }
|
|
1203
|
+
: {}),
|
|
1204
|
+
...("mode" in input
|
|
1205
|
+
? {
|
|
1206
|
+
mode: input.mode,
|
|
1207
|
+
sourceUpdateIds: input.sourceUpdateIds,
|
|
1208
|
+
slot: input.slot,
|
|
1209
|
+
target: input.target,
|
|
1210
|
+
oldTarget: input.oldTarget,
|
|
1211
|
+
}
|
|
1212
|
+
: { updates: input.updates }),
|
|
1213
|
+
});
|
|
1214
|
+
const observing = "mode" in expected && expected.mode === "observe";
|
|
1215
|
+
const commandObserving = "mode" in expected && expected.mode === "observe-command";
|
|
1216
|
+
const settling = "mode" in expected &&
|
|
1217
|
+
(expected.mode === "release" ||
|
|
1218
|
+
expected.mode === "discard" ||
|
|
1219
|
+
observing ||
|
|
1220
|
+
commandObserving);
|
|
1221
|
+
const required = [
|
|
1222
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
|
|
1223
|
+
...("mode" in expected || held
|
|
1224
|
+
? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY]
|
|
1225
|
+
: []),
|
|
1226
|
+
...(settling || held ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
|
|
1227
|
+
...(held
|
|
1228
|
+
? [
|
|
1229
|
+
TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
|
|
1230
|
+
TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
|
|
1231
|
+
]
|
|
1232
|
+
: []),
|
|
1233
|
+
];
|
|
1234
|
+
const follower = deps.getFollower(expected.instanceId), secret = deps.getAuthSecret();
|
|
1235
|
+
if (!isCurrent() ||
|
|
1236
|
+
!secret ||
|
|
1237
|
+
!follower?.registrationGeneration ||
|
|
1238
|
+
!follower.busSocketPath ||
|
|
1239
|
+
follower.sessionId !== expected.sessionId ||
|
|
1240
|
+
!hasTelegramBusSharedCapabilities(deps.localProtocolIdentity, follower.protocol, required))
|
|
1241
|
+
return undefined;
|
|
1242
|
+
const captured = structuredClone(follower);
|
|
1243
|
+
/** The first failed authority condition, by fixed label; undefined while the captured recipient is current. */
|
|
1244
|
+
const staleReason = () => {
|
|
1245
|
+
if (!isCurrent())
|
|
1246
|
+
return "caller authority";
|
|
1247
|
+
if (deps.getAuthSecret() !== secret)
|
|
1248
|
+
return "auth secret";
|
|
1249
|
+
if (!isDeepStrictEqual(deps.localProtocolIdentity, localProtocol) ||
|
|
1250
|
+
!required.every((cap) => hasTelegramBusCapability(deps.localProtocolIdentity, cap)))
|
|
1251
|
+
return "local protocol";
|
|
1252
|
+
const live = deps.getFollower(expected.instanceId);
|
|
1253
|
+
if (!live)
|
|
1254
|
+
return "follower registration";
|
|
1255
|
+
if (live.sessionId !== captured.sessionId ||
|
|
1256
|
+
live.registrationGeneration !== captured.registrationGeneration ||
|
|
1257
|
+
live.sessionGeneration !== captured.sessionGeneration)
|
|
1258
|
+
return "follower generation";
|
|
1259
|
+
if (live.pid !== captured.pid ||
|
|
1260
|
+
live.processBirthId !== captured.processBirthId)
|
|
1261
|
+
return "follower process";
|
|
1262
|
+
if (live.busSocketPath !== captured.busSocketPath ||
|
|
1263
|
+
live.slot !== captured.slot ||
|
|
1264
|
+
live.cwd !== captured.cwd)
|
|
1265
|
+
return "follower endpoint";
|
|
1266
|
+
if ("mode" in expected
|
|
1267
|
+
? live.slot !== expected.slot ||
|
|
1268
|
+
!live.target ||
|
|
1269
|
+
(!areTelegramTargetsEqual(live.target, expected.target) &&
|
|
1270
|
+
!areTelegramTargetsEqual(live.target, expected.oldTarget))
|
|
1271
|
+
: !isDeepStrictEqual(live.target, captured.target))
|
|
1272
|
+
return "follower target";
|
|
1273
|
+
if (!required.every((cap) => hasTelegramBusCapability(live.protocol, cap)) ||
|
|
1274
|
+
!isDeepStrictEqual(live.protocol, captured.protocol) ||
|
|
1275
|
+
!getTelegramBusProtocolCompatibility({
|
|
1276
|
+
local: deps.localProtocolIdentity,
|
|
1277
|
+
remote: live.protocol,
|
|
1278
|
+
}).compatible)
|
|
1279
|
+
return "follower protocol";
|
|
1280
|
+
return undefined;
|
|
1281
|
+
};
|
|
1282
|
+
const current = () => staleReason() === undefined;
|
|
1283
|
+
if (!current())
|
|
1284
|
+
return undefined;
|
|
1285
|
+
const requestId = deps.createRequestId();
|
|
1286
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
1287
|
+
socketPath: captured.busSocketPath,
|
|
1288
|
+
timeoutMs: deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CONTROL_TIMEOUT_MS,
|
|
1289
|
+
retry: { attempts: 1, delayMs: 0 },
|
|
1290
|
+
...(held
|
|
1291
|
+
? {
|
|
1292
|
+
assertAuthority: () => {
|
|
1293
|
+
const reason = staleReason();
|
|
1294
|
+
if (reason)
|
|
1295
|
+
throw new Error(`Selected held input sender authority changed (${reason}).`);
|
|
1296
|
+
},
|
|
1297
|
+
}
|
|
1298
|
+
: {}),
|
|
1299
|
+
envelope: {
|
|
1300
|
+
requestId,
|
|
1301
|
+
auth: secret,
|
|
1302
|
+
...(held
|
|
1303
|
+
? {
|
|
1304
|
+
...marker,
|
|
1305
|
+
...("mode" in expected
|
|
1306
|
+
? { preparedSource: expected.preparedSource }
|
|
1307
|
+
: {}),
|
|
1308
|
+
}
|
|
1309
|
+
: {}),
|
|
1310
|
+
recipientInstanceId: expected.instanceId,
|
|
1311
|
+
recipientRegistrationGeneration: captured.registrationGeneration,
|
|
1312
|
+
recipientSessionId: expected.sessionId,
|
|
1313
|
+
recipientBindingKey: expected.recipientBindingKey,
|
|
1314
|
+
operationId: expected.operationId,
|
|
1315
|
+
sentAtMs: Date.now(),
|
|
1316
|
+
...("mode" in expected
|
|
1317
|
+
? settling
|
|
1318
|
+
? {
|
|
1319
|
+
kind: "leader.settleLiveRebind",
|
|
1320
|
+
mode: expected.mode,
|
|
1321
|
+
sourceUpdateIds: expected.sourceUpdateIds,
|
|
1322
|
+
...(observing ? { oldTarget: expected.oldTarget } : {}),
|
|
1323
|
+
}
|
|
1324
|
+
: {
|
|
1325
|
+
kind: "leader.applyLiveRebind",
|
|
1326
|
+
mode: expected.mode,
|
|
1327
|
+
sourceUpdateIds: expected.sourceUpdateIds,
|
|
1328
|
+
}
|
|
1329
|
+
: {
|
|
1330
|
+
kind: "leader.prepareLiveRebind",
|
|
1331
|
+
updates: expected.updates,
|
|
1332
|
+
}),
|
|
1333
|
+
},
|
|
1334
|
+
});
|
|
1335
|
+
if (!current() ||
|
|
1336
|
+
response?.kind !== "bus.ack" ||
|
|
1337
|
+
response.requestId !== requestId ||
|
|
1338
|
+
response.ok !== true ||
|
|
1339
|
+
!isRecord(response.result))
|
|
1340
|
+
return undefined;
|
|
1341
|
+
const result = response.result, recipient = result.recipient;
|
|
1342
|
+
if (!hasOnlyWireKeys(result, [
|
|
1343
|
+
"operationId",
|
|
1344
|
+
"recipient",
|
|
1345
|
+
"sourceUpdateIds",
|
|
1346
|
+
"selectedCommand",
|
|
1347
|
+
"preparedSource",
|
|
1348
|
+
"status",
|
|
1349
|
+
"command",
|
|
1350
|
+
"sourceAck",
|
|
1351
|
+
"target",
|
|
1352
|
+
"slot",
|
|
1353
|
+
"action",
|
|
1354
|
+
"oldTarget",
|
|
1355
|
+
"work",
|
|
1356
|
+
]))
|
|
1357
|
+
return undefined;
|
|
1358
|
+
if (selectedCommand
|
|
1359
|
+
? !isDeepStrictEqual(parseSelectedCommandInput(result.selectedCommand), selectedCommand)
|
|
1360
|
+
: Object.hasOwn(result, "selectedCommand"))
|
|
1361
|
+
return undefined;
|
|
1362
|
+
const ids = ("mode" in expected
|
|
1363
|
+
? [...expected.sourceUpdateIds]
|
|
1364
|
+
: expected.updates.map((update) => update.update_id)).sort((a, b) => a - b);
|
|
1365
|
+
const observedSource = parsePreparedCommandSource(result.preparedSource);
|
|
1366
|
+
if (held
|
|
1367
|
+
? !observedSource ||
|
|
1368
|
+
observedSource.updateId !== ids[0] ||
|
|
1369
|
+
observedSource.journalBindingKey !== expected.recipientBindingKey ||
|
|
1370
|
+
(expected.preparedSource &&
|
|
1371
|
+
!isDeepStrictEqual(observedSource, expected.preparedSource))
|
|
1372
|
+
: Object.hasOwn(result, "preparedSource"))
|
|
1373
|
+
return undefined;
|
|
1374
|
+
if (result.operationId !== expected.operationId ||
|
|
1375
|
+
!(commandObserving
|
|
1376
|
+
? result.status === "command-observed"
|
|
1377
|
+
: observing
|
|
1378
|
+
? result.status === "observed"
|
|
1379
|
+
: settling
|
|
1380
|
+
? result.status === "protected" ||
|
|
1381
|
+
result.status === "unknown" ||
|
|
1382
|
+
result.status ===
|
|
1383
|
+
(expected.mode === "release" ? "released" : "discarded")
|
|
1384
|
+
: "mode" in expected
|
|
1385
|
+
? result.status === "saved" || result.status === "applied"
|
|
1386
|
+
: result.status === "saved") ||
|
|
1387
|
+
!isRecord(recipient) ||
|
|
1388
|
+
recipient.instanceId !== expected.instanceId ||
|
|
1389
|
+
recipient.sessionId !== expected.sessionId ||
|
|
1390
|
+
recipient.generation !== captured.registrationGeneration ||
|
|
1391
|
+
recipient.bindingKey !== expected.recipientBindingKey ||
|
|
1392
|
+
!Array.isArray(result.sourceUpdateIds) ||
|
|
1393
|
+
result.sourceUpdateIds.length !== ids.length ||
|
|
1394
|
+
result.sourceUpdateIds.some((id, index) => id !== ids[index]))
|
|
1395
|
+
return undefined;
|
|
1396
|
+
const observation = {
|
|
1397
|
+
operationId: expected.operationId,
|
|
1398
|
+
recipient: {
|
|
1399
|
+
instanceId: expected.instanceId,
|
|
1400
|
+
sessionId: expected.sessionId,
|
|
1401
|
+
generation: captured.registrationGeneration,
|
|
1402
|
+
bindingKey: expected.recipientBindingKey,
|
|
1403
|
+
},
|
|
1404
|
+
sourceUpdateIds: ids,
|
|
1405
|
+
...(held
|
|
1406
|
+
? { ...structuredClone(marker), preparedSource: { ...observedSource } }
|
|
1407
|
+
: {}),
|
|
1408
|
+
status: "saved",
|
|
1409
|
+
};
|
|
1410
|
+
if (!("mode" in expected))
|
|
1411
|
+
return observation;
|
|
1412
|
+
if (commandObserving) {
|
|
1413
|
+
const sourceAck = parsePreparedCommandSource(result.sourceAck);
|
|
1414
|
+
if (!hasOnlyWireKeys(result, [
|
|
1415
|
+
"operationId",
|
|
1416
|
+
"recipient",
|
|
1417
|
+
"sourceUpdateIds",
|
|
1418
|
+
"selectedCommand",
|
|
1419
|
+
"preparedSource",
|
|
1420
|
+
"status",
|
|
1421
|
+
"command",
|
|
1422
|
+
"sourceAck",
|
|
1423
|
+
]) ||
|
|
1424
|
+
!hasOnlyWireKeys(recipient, [
|
|
1425
|
+
"instanceId",
|
|
1426
|
+
"sessionId",
|
|
1427
|
+
"generation",
|
|
1428
|
+
"bindingKey",
|
|
1429
|
+
]) ||
|
|
1430
|
+
!["completed", "unknown", "not-issued"].includes(result.command) ||
|
|
1431
|
+
(Object.hasOwn(result, "sourceAck") &&
|
|
1432
|
+
(!sourceAck ||
|
|
1433
|
+
!isDeepStrictEqual(sourceAck, expected.preparedSource) ||
|
|
1434
|
+
result.command === "not-issued")))
|
|
1435
|
+
return undefined;
|
|
1436
|
+
const settled = {
|
|
1437
|
+
command: result.command,
|
|
1438
|
+
...(sourceAck ? { sourceAck: { ...sourceAck } } : {}),
|
|
1439
|
+
};
|
|
1440
|
+
return {
|
|
1441
|
+
...observation,
|
|
1442
|
+
status: "command-observed",
|
|
1443
|
+
selectedCommand: structuredClone(selectedCommand),
|
|
1444
|
+
preparedSource: { ...observedSource },
|
|
1445
|
+
...settled,
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
if (observing) {
|
|
1449
|
+
const work = result.work, oldTarget = parseThreadTarget(result.oldTarget);
|
|
1450
|
+
if (!oldTarget ||
|
|
1451
|
+
!isDeepStrictEqual(oldTarget, expected.oldTarget) ||
|
|
1452
|
+
!isRecord(work) ||
|
|
1453
|
+
[
|
|
1454
|
+
work.sessionBusy,
|
|
1455
|
+
work.targetWork,
|
|
1456
|
+
work.deliveryPending,
|
|
1457
|
+
work.unknown,
|
|
1458
|
+
].some((flag) => typeof flag !== "boolean"))
|
|
1459
|
+
return undefined;
|
|
1460
|
+
return {
|
|
1461
|
+
...observation,
|
|
1462
|
+
status: "observed",
|
|
1463
|
+
oldTarget,
|
|
1464
|
+
work: {
|
|
1465
|
+
sessionBusy: work.sessionBusy,
|
|
1466
|
+
targetWork: work.targetWork,
|
|
1467
|
+
deliveryPending: work.deliveryPending,
|
|
1468
|
+
unknown: work.unknown,
|
|
1469
|
+
},
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
if (settling) {
|
|
1473
|
+
if (result.action !== expected.mode)
|
|
1474
|
+
return undefined;
|
|
1475
|
+
return {
|
|
1476
|
+
...observation,
|
|
1477
|
+
action: expected.mode,
|
|
1478
|
+
status: result.status,
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
const target = parseThreadTarget(result.target), wanted = result.status === "applied" ? expected.target : expected.oldTarget;
|
|
1482
|
+
if (!target ||
|
|
1483
|
+
!isDeepStrictEqual(target, wanted) ||
|
|
1484
|
+
result.slot !== expected.slot)
|
|
954
1485
|
return undefined;
|
|
955
|
-
return {
|
|
956
|
-
|
|
1486
|
+
return {
|
|
1487
|
+
...observation,
|
|
1488
|
+
status: result.status,
|
|
1489
|
+
target,
|
|
1490
|
+
slot: expected.slot,
|
|
1491
|
+
};
|
|
957
1492
|
};
|
|
958
1493
|
}
|
|
959
1494
|
export function isTelegramBusEnvelopeAuthorized(envelope, secret) {
|
|
@@ -965,13 +1500,12 @@ export function isTelegramBusEnvelopeAuthorized(envelope, secret) {
|
|
|
965
1500
|
const expected = Buffer.from(secret);
|
|
966
1501
|
return auth.length === expected.length && timingSafeEqual(auth, expected);
|
|
967
1502
|
}
|
|
1503
|
+
/** Negative acknowledgement carrying only a diagnostic message. */
|
|
1504
|
+
export function rejectTelegramBusRequest(requestId, message) {
|
|
1505
|
+
return { kind: "bus.ack", requestId, ok: false, message };
|
|
1506
|
+
}
|
|
968
1507
|
export function createUnauthorizedBusAck(requestId) {
|
|
969
|
-
return
|
|
970
|
-
kind: "bus.ack",
|
|
971
|
-
requestId,
|
|
972
|
-
ok: false,
|
|
973
|
-
message: "Unauthorized Telegram bus envelope.",
|
|
974
|
-
};
|
|
1508
|
+
return rejectTelegramBusRequest(requestId, "Unauthorized Telegram bus envelope.");
|
|
975
1509
|
}
|
|
976
1510
|
export function createTelegramBusLocalServer(deps) {
|
|
977
1511
|
const requestLedger = new Map();
|
|
@@ -1022,7 +1556,9 @@ export function createTelegramBusLocalServer(deps) {
|
|
|
1022
1556
|
result: Promise.resolve().then(() => deps.handleEnvelope(envelope)),
|
|
1023
1557
|
};
|
|
1024
1558
|
requestLedger.set(key, entry);
|
|
1025
|
-
const settled = () => {
|
|
1559
|
+
const settled = () => {
|
|
1560
|
+
entry.settled = true;
|
|
1561
|
+
};
|
|
1026
1562
|
void entry.result.then(settled, settled);
|
|
1027
1563
|
return entry.result;
|
|
1028
1564
|
};
|
|
@@ -1246,6 +1782,24 @@ function getTelegramBusEnvelopeDiagnostics(envelope) {
|
|
|
1246
1782
|
function sendTelegramBusLocalEnvelopeOnce(options) {
|
|
1247
1783
|
const timeoutMs = options.timeoutMs ?? 1000;
|
|
1248
1784
|
return new Promise((resolve, reject) => {
|
|
1785
|
+
const assertAuthority = options.assertAuthority;
|
|
1786
|
+
let issued = false;
|
|
1787
|
+
const assertCurrent = () => {
|
|
1788
|
+
try {
|
|
1789
|
+
assertAuthority?.();
|
|
1790
|
+
}
|
|
1791
|
+
catch (error) {
|
|
1792
|
+
// The caller's reason is a fixed condition label, never request or credential content.
|
|
1793
|
+
throw new TelegramBusLocalAuthorityError(issued, error instanceof Error ? error.message : undefined);
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
try {
|
|
1797
|
+
assertCurrent();
|
|
1798
|
+
}
|
|
1799
|
+
catch (error) {
|
|
1800
|
+
reject(error);
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1249
1803
|
const socket = createConnection(options.socketPath);
|
|
1250
1804
|
let settled = false;
|
|
1251
1805
|
let buffer = "";
|
|
@@ -1274,7 +1828,16 @@ function sendTelegramBusLocalEnvelopeOnce(options) {
|
|
|
1274
1828
|
}, timeoutMs);
|
|
1275
1829
|
socket.setEncoding("utf8");
|
|
1276
1830
|
socket.once("connect", () => {
|
|
1277
|
-
|
|
1831
|
+
try {
|
|
1832
|
+
assertCurrent();
|
|
1833
|
+
const encoded = encodeTelegramBusEnvelope(options.envelope);
|
|
1834
|
+
assertCurrent();
|
|
1835
|
+
issued = true;
|
|
1836
|
+
socket.write(encoded);
|
|
1837
|
+
}
|
|
1838
|
+
catch (error) {
|
|
1839
|
+
settle(() => reject(error));
|
|
1840
|
+
}
|
|
1278
1841
|
});
|
|
1279
1842
|
socket.on("data", (chunk) => {
|
|
1280
1843
|
buffer += chunk;
|
|
@@ -1282,7 +1845,14 @@ function sendTelegramBusLocalEnvelopeOnce(options) {
|
|
|
1282
1845
|
if (newlineIndex < 0)
|
|
1283
1846
|
return;
|
|
1284
1847
|
const line = buffer.slice(0, newlineIndex);
|
|
1285
|
-
|
|
1848
|
+
try {
|
|
1849
|
+
const response = parseTelegramBusEnvelope(line);
|
|
1850
|
+
assertCurrent();
|
|
1851
|
+
settle(() => resolve(response));
|
|
1852
|
+
}
|
|
1853
|
+
catch (error) {
|
|
1854
|
+
settle(() => reject(error));
|
|
1855
|
+
}
|
|
1286
1856
|
});
|
|
1287
1857
|
socket.once("error", (error) => settle(() => reject(error)));
|
|
1288
1858
|
socket.once("end", () => settle(() => resolve(undefined)));
|
|
@@ -1308,7 +1878,9 @@ export async function sendTelegramBusLocalEnvelope(options) {
|
|
|
1308
1878
|
attempts,
|
|
1309
1879
|
...info,
|
|
1310
1880
|
});
|
|
1311
|
-
if (
|
|
1881
|
+
if (error instanceof TelegramBusLocalAuthorityError ||
|
|
1882
|
+
attempt >= attempts ||
|
|
1883
|
+
!isRetryableTelegramBusTransportError(error)) {
|
|
1312
1884
|
throw error;
|
|
1313
1885
|
}
|
|
1314
1886
|
resolvedOptions.recordTransportEvent?.("client-retry", {
|
|
@@ -1324,21 +1896,27 @@ export async function sendTelegramBusLocalEnvelope(options) {
|
|
|
1324
1896
|
}
|
|
1325
1897
|
}
|
|
1326
1898
|
export function createTelegramBusForwardOwnershipValidator(registry) {
|
|
1327
|
-
return ownership => {
|
|
1899
|
+
return (ownership) => {
|
|
1328
1900
|
const follower = registry.get(ownership.instanceId);
|
|
1329
|
-
return isTelegramBusForwardOwnershipCurrent(ownership, follower?.registrationGeneration &&
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1901
|
+
return isTelegramBusForwardOwnershipCurrent(ownership, follower?.registrationGeneration &&
|
|
1902
|
+
follower.profileKey &&
|
|
1903
|
+
follower.protocol
|
|
1904
|
+
? {
|
|
1905
|
+
instanceId: follower.instanceId,
|
|
1906
|
+
ownerGeneration: follower.registrationGeneration,
|
|
1907
|
+
recipientBindingKey: follower.profileKey,
|
|
1908
|
+
protocolIdentity: follower.protocol,
|
|
1909
|
+
}
|
|
1910
|
+
: undefined);
|
|
1335
1911
|
};
|
|
1336
1912
|
}
|
|
1337
1913
|
function hasTelegramBusFollowerIdentityOverlap(first, second) {
|
|
1338
|
-
return first.instanceId === second.instanceId ||
|
|
1339
|
-
(first.profileKey !== undefined &&
|
|
1340
|
-
|
|
1341
|
-
|
|
1914
|
+
return (first.instanceId === second.instanceId ||
|
|
1915
|
+
(first.profileKey !== undefined &&
|
|
1916
|
+
first.profileKey === second.profileKey) ||
|
|
1917
|
+
(first.target !== undefined &&
|
|
1918
|
+
first.target.chatId === second.target?.chatId &&
|
|
1919
|
+
first.target.threadId === second.target.threadId));
|
|
1342
1920
|
}
|
|
1343
1921
|
export function createTelegramBusFollowerRegistry() {
|
|
1344
1922
|
const followers = new Map();
|
|
@@ -1417,8 +1995,10 @@ export function createTelegramBusFollowerRegistry() {
|
|
|
1417
1995
|
},
|
|
1418
1996
|
observeUnregistered: (follower) => {
|
|
1419
1997
|
// This watches replacement, not process death or delivery authority.
|
|
1420
|
-
const observation = {
|
|
1421
|
-
|
|
1998
|
+
const observation = {
|
|
1999
|
+
follower: clone(follower),
|
|
2000
|
+
current: ![...followers.values()].some((current) => hasTelegramBusFollowerIdentityOverlap(current, follower)),
|
|
2001
|
+
};
|
|
1422
2002
|
if (observation.current)
|
|
1423
2003
|
observations.add(observation);
|
|
1424
2004
|
return {
|
|
@@ -1468,37 +2048,18 @@ async function handleTelegramBusSocketLine(line, socket, handleEnvelope, recordT
|
|
|
1468
2048
|
...getTelegramBusEnvelopeDiagnostics(envelope),
|
|
1469
2049
|
...classifyTelegramBusTransportError(error),
|
|
1470
2050
|
});
|
|
1471
|
-
socket.write(encodeTelegramBusEnvelope(
|
|
1472
|
-
kind: "bus.ack",
|
|
1473
|
-
requestId: envelope.requestId,
|
|
1474
|
-
ok: false,
|
|
1475
|
-
message: "Telegram bus handler failed.",
|
|
1476
|
-
}));
|
|
2051
|
+
socket.write(encodeTelegramBusEnvelope(rejectTelegramBusRequest(envelope.requestId, "Telegram bus handler failed.")));
|
|
1477
2052
|
}
|
|
1478
2053
|
}
|
|
1479
2054
|
function parseRegisterEnvelope(value, requestId, kind) {
|
|
1480
2055
|
const registration = parseRegistration(value.registration);
|
|
1481
2056
|
return registration ? { kind, requestId, registration } : undefined;
|
|
1482
2057
|
}
|
|
1483
|
-
function
|
|
2058
|
+
function parseFollowerPresenceEnvelope(value, requestId, kind) {
|
|
1484
2059
|
return typeof value.instanceId === "string" &&
|
|
1485
2060
|
typeof value.sentAtMs === "number"
|
|
1486
2061
|
? {
|
|
1487
|
-
kind
|
|
1488
|
-
requestId,
|
|
1489
|
-
instanceId: value.instanceId,
|
|
1490
|
-
...(typeof value.registrationGeneration === "string"
|
|
1491
|
-
? { registrationGeneration: value.registrationGeneration }
|
|
1492
|
-
: {}),
|
|
1493
|
-
sentAtMs: value.sentAtMs,
|
|
1494
|
-
}
|
|
1495
|
-
: undefined;
|
|
1496
|
-
}
|
|
1497
|
-
function parseDisconnectEnvelope(value, requestId) {
|
|
1498
|
-
return typeof value.instanceId === "string" &&
|
|
1499
|
-
typeof value.sentAtMs === "number"
|
|
1500
|
-
? {
|
|
1501
|
-
kind: "follower.disconnect",
|
|
2062
|
+
kind,
|
|
1502
2063
|
requestId,
|
|
1503
2064
|
instanceId: value.instanceId,
|
|
1504
2065
|
...(typeof value.registrationGeneration === "string"
|
|
@@ -1536,14 +2097,17 @@ function parseTelegramBusFollowerDeliveryIdentity(value) {
|
|
|
1536
2097
|
typeof value.recipientBindingKey !== "string" ||
|
|
1537
2098
|
!value.recipientBindingKey ||
|
|
1538
2099
|
(value.sourceRecoveryKey !== undefined &&
|
|
1539
|
-
(typeof value.sourceRecoveryKey !== "string" ||
|
|
2100
|
+
(typeof value.sourceRecoveryKey !== "string" ||
|
|
2101
|
+
!value.sourceRecoveryKey ||
|
|
1540
2102
|
value.sourceRecoveryKey.length > 1_024)) ||
|
|
1541
2103
|
(value.sourceClaim !== undefined &&
|
|
1542
2104
|
(!isRecord(value.sourceClaim) ||
|
|
1543
2105
|
typeof value.sourceClaim.acquisitionId !== "string" ||
|
|
1544
|
-
!value.sourceClaim.acquisitionId ||
|
|
2106
|
+
!value.sourceClaim.acquisitionId ||
|
|
2107
|
+
value.sourceClaim.acquisitionId.length > 256 ||
|
|
1545
2108
|
typeof value.sourceClaim.handoffId !== "string" ||
|
|
1546
|
-
!value.sourceClaim.handoffId ||
|
|
2109
|
+
!value.sourceClaim.handoffId ||
|
|
2110
|
+
value.sourceClaim.handoffId.length > 256))) {
|
|
1547
2111
|
return undefined;
|
|
1548
2112
|
}
|
|
1549
2113
|
return {
|
|
@@ -1551,10 +2115,16 @@ function parseTelegramBusFollowerDeliveryIdentity(value) {
|
|
|
1551
2115
|
sourceUpdateId: value.sourceUpdateId,
|
|
1552
2116
|
recipientBindingKey: value.recipientBindingKey,
|
|
1553
2117
|
...(typeof value.sourceRecoveryKey === "string"
|
|
1554
|
-
? { sourceRecoveryKey: value.sourceRecoveryKey }
|
|
2118
|
+
? { sourceRecoveryKey: value.sourceRecoveryKey }
|
|
2119
|
+
: {}),
|
|
1555
2120
|
...(isRecord(value.sourceClaim)
|
|
1556
|
-
? {
|
|
1557
|
-
|
|
2121
|
+
? {
|
|
2122
|
+
sourceClaim: {
|
|
2123
|
+
acquisitionId: value.sourceClaim.acquisitionId,
|
|
2124
|
+
handoffId: value.sourceClaim.handoffId,
|
|
2125
|
+
},
|
|
2126
|
+
}
|
|
2127
|
+
: {}),
|
|
1558
2128
|
};
|
|
1559
2129
|
}
|
|
1560
2130
|
function parseForwardCallbackEnvelope(value, requestId) {
|
|
@@ -1617,37 +2187,58 @@ function parseForwardMessageEnvelope(value, requestId, kind) {
|
|
|
1617
2187
|
}
|
|
1618
2188
|
function parseOfferInputCustodyHandoffEnvelope(value, requestId) {
|
|
1619
2189
|
const source = value.source;
|
|
1620
|
-
if (!isRecord(source) ||
|
|
2190
|
+
if (!isRecord(source) ||
|
|
2191
|
+
typeof value.recipientInstanceId !== "string" ||
|
|
1621
2192
|
typeof value.recipientRegistrationGeneration !== "string" ||
|
|
1622
|
-
typeof value.recipientBindingKey !== "string" ||
|
|
1623
|
-
|
|
2193
|
+
typeof value.recipientBindingKey !== "string" ||
|
|
2194
|
+
!value.recipientBindingKey ||
|
|
2195
|
+
typeof value.sourceRecoveryKey !== "string" ||
|
|
2196
|
+
!value.sourceRecoveryKey ||
|
|
1624
2197
|
value.sourceRecoveryKey.length > 1_024 ||
|
|
1625
|
-
typeof source.journalBindingKey !== "string" ||
|
|
2198
|
+
typeof source.journalBindingKey !== "string" ||
|
|
2199
|
+
!source.journalBindingKey ||
|
|
1626
2200
|
source.journalBindingKey !== value.sourceRecoveryKey ||
|
|
1627
|
-
typeof source.tokenSha256 !== "string" ||
|
|
1628
|
-
|
|
1629
|
-
|
|
2201
|
+
typeof source.tokenSha256 !== "string" ||
|
|
2202
|
+
!/^[a-f0-9]{64}$/u.test(source.tokenSha256) ||
|
|
2203
|
+
!Number.isSafeInteger(source.updateId) ||
|
|
2204
|
+
source.updateId < 0 ||
|
|
2205
|
+
typeof value.handoffId !== "string" ||
|
|
2206
|
+
!value.handoffId ||
|
|
2207
|
+
value.handoffId.length > 256 ||
|
|
1630
2208
|
typeof value.sentAtMs !== "number")
|
|
1631
2209
|
return undefined;
|
|
1632
|
-
return {
|
|
2210
|
+
return {
|
|
2211
|
+
kind: "leader.offerInputCustodyHandoff",
|
|
2212
|
+
requestId,
|
|
1633
2213
|
recipientInstanceId: value.recipientInstanceId,
|
|
1634
2214
|
recipientRegistrationGeneration: value.recipientRegistrationGeneration,
|
|
1635
2215
|
recipientBindingKey: value.recipientBindingKey,
|
|
1636
2216
|
sourceRecoveryKey: value.sourceRecoveryKey,
|
|
1637
|
-
source: {
|
|
1638
|
-
|
|
1639
|
-
|
|
2217
|
+
source: {
|
|
2218
|
+
journalBindingKey: source.journalBindingKey,
|
|
2219
|
+
tokenSha256: source.tokenSha256,
|
|
2220
|
+
updateId: source.updateId,
|
|
2221
|
+
},
|
|
2222
|
+
handoffId: value.handoffId,
|
|
2223
|
+
sentAtMs: value.sentAtMs,
|
|
2224
|
+
};
|
|
1640
2225
|
}
|
|
1641
2226
|
function parseWakeInputCustodyEnvelope(value, requestId) {
|
|
1642
2227
|
const delivery = parseTelegramBusFollowerDeliveryIdentity(value.delivery);
|
|
1643
|
-
return delivery &&
|
|
2228
|
+
return delivery &&
|
|
2229
|
+
delivery.sourceRecoveryKey &&
|
|
2230
|
+
delivery.sourceClaim &&
|
|
1644
2231
|
typeof value.recipientInstanceId === "string" &&
|
|
1645
2232
|
typeof value.recipientRegistrationGeneration === "string" &&
|
|
1646
2233
|
typeof value.sentAtMs === "number"
|
|
1647
|
-
? {
|
|
2234
|
+
? {
|
|
2235
|
+
kind: "leader.wakeInputCustody",
|
|
2236
|
+
requestId,
|
|
1648
2237
|
recipientInstanceId: value.recipientInstanceId,
|
|
1649
2238
|
recipientRegistrationGeneration: value.recipientRegistrationGeneration,
|
|
1650
|
-
delivery,
|
|
2239
|
+
delivery,
|
|
2240
|
+
sentAtMs: value.sentAtMs,
|
|
2241
|
+
}
|
|
1651
2242
|
: undefined;
|
|
1652
2243
|
}
|
|
1653
2244
|
function parseQueueHandoffEnvelope(value, requestId, kind) {
|
|
@@ -1777,6 +2368,144 @@ function parseRouteAgentMessageEnvelope(value, requestId) {
|
|
|
1777
2368
|
sentAtMs: value.sentAtMs,
|
|
1778
2369
|
};
|
|
1779
2370
|
}
|
|
2371
|
+
const nonempty = (input, limit = 128) => typeof input === "string" && !!input.trim() && input.length <= limit;
|
|
2372
|
+
const positive = (input) => Number.isSafeInteger(input) && input > 0;
|
|
2373
|
+
const nonnegative = (input) => Number.isSafeInteger(input) && input >= 0;
|
|
2374
|
+
/** Shared closed text-effect codec; parsing alone supplies no recipient or issuance authority. */
|
|
2375
|
+
export function parseTelegramBusSelectedMenuTextEffect(effect) {
|
|
2376
|
+
if (!isRecord(effect) ||
|
|
2377
|
+
(effect.kind !== "send-text" && effect.kind !== "edit-text") ||
|
|
2378
|
+
!hasOnlyWireKeys(effect, [
|
|
2379
|
+
"kind",
|
|
2380
|
+
"text",
|
|
2381
|
+
"parseMode",
|
|
2382
|
+
"replyMarkup",
|
|
2383
|
+
...(effect.kind === "send-text" ? ["replyToMessageId"] : ["messageId"]),
|
|
2384
|
+
]) ||
|
|
2385
|
+
!nonempty(effect.text, 4096) ||
|
|
2386
|
+
(effect.parseMode !== undefined && effect.parseMode !== "HTML") ||
|
|
2387
|
+
(effect.kind === "send-text"
|
|
2388
|
+
? effect.replyToMessageId !== undefined &&
|
|
2389
|
+
!positive(effect.replyToMessageId)
|
|
2390
|
+
: !positive(effect.messageId)))
|
|
2391
|
+
return undefined;
|
|
2392
|
+
let replyMarkup;
|
|
2393
|
+
if (effect.replyMarkup !== undefined) {
|
|
2394
|
+
const markup = effect.replyMarkup;
|
|
2395
|
+
if (!isRecord(markup) ||
|
|
2396
|
+
!hasOnlyWireKeys(markup, ["inline_keyboard"]) ||
|
|
2397
|
+
!Array.isArray(markup.inline_keyboard) ||
|
|
2398
|
+
markup.inline_keyboard.length > 32 ||
|
|
2399
|
+
markup.inline_keyboard.some((row) => !Array.isArray(row) ||
|
|
2400
|
+
!row.length ||
|
|
2401
|
+
row.length > 8 ||
|
|
2402
|
+
row.some((button) => !isRecord(button) ||
|
|
2403
|
+
!hasOnlyWireKeys(button, ["text", "callback_data"]) ||
|
|
2404
|
+
!nonempty(button.text) ||
|
|
2405
|
+
!nonempty(button.callback_data, 64) ||
|
|
2406
|
+
Buffer.byteLength(button.callback_data, "utf8") > 64)))
|
|
2407
|
+
return undefined;
|
|
2408
|
+
replyMarkup = {
|
|
2409
|
+
inline_keyboard: markup.inline_keyboard.map((row) => row.map((button) => ({
|
|
2410
|
+
text: button.text,
|
|
2411
|
+
callback_data: button.callback_data,
|
|
2412
|
+
}))),
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
const common = {
|
|
2416
|
+
text: effect.text,
|
|
2417
|
+
...(effect.parseMode === "HTML" ? { parseMode: "HTML" } : {}),
|
|
2418
|
+
...(replyMarkup ? { replyMarkup } : {}),
|
|
2419
|
+
};
|
|
2420
|
+
return effect.kind === "send-text"
|
|
2421
|
+
? {
|
|
2422
|
+
...common,
|
|
2423
|
+
kind: "send-text",
|
|
2424
|
+
...(effect.replyToMessageId !== undefined
|
|
2425
|
+
? { replyToMessageId: effect.replyToMessageId }
|
|
2426
|
+
: {}),
|
|
2427
|
+
}
|
|
2428
|
+
: { ...common, kind: "edit-text", messageId: effect.messageId };
|
|
2429
|
+
}
|
|
2430
|
+
function parseSelectedMenuDeliveryEnvelope(value, requestId) {
|
|
2431
|
+
const recipient = value.recipient, executor = value.executor, effect = parseTelegramBusSelectedMenuTextEffect(value.effect);
|
|
2432
|
+
if (!hasOnlyWireKeys(value, [
|
|
2433
|
+
"kind",
|
|
2434
|
+
"requestId",
|
|
2435
|
+
"auth",
|
|
2436
|
+
"instanceId",
|
|
2437
|
+
"registrationGeneration",
|
|
2438
|
+
"operationId",
|
|
2439
|
+
"recipient",
|
|
2440
|
+
"executor",
|
|
2441
|
+
"operatorUserId",
|
|
2442
|
+
"effect",
|
|
2443
|
+
"sentAtMs",
|
|
2444
|
+
]) ||
|
|
2445
|
+
!nonempty(requestId) ||
|
|
2446
|
+
!nonempty(value.instanceId) ||
|
|
2447
|
+
!nonempty(value.registrationGeneration) ||
|
|
2448
|
+
!nonempty(value.operationId) ||
|
|
2449
|
+
!isRecord(recipient) ||
|
|
2450
|
+
!hasOnlyWireKeys(recipient, [
|
|
2451
|
+
"sessionId",
|
|
2452
|
+
"sessionGeneration",
|
|
2453
|
+
"processId",
|
|
2454
|
+
"processBirthId",
|
|
2455
|
+
"profileKey",
|
|
2456
|
+
"journalBindingKey",
|
|
2457
|
+
"target",
|
|
2458
|
+
]) ||
|
|
2459
|
+
!nonempty(recipient.sessionId) ||
|
|
2460
|
+
!nonnegative(recipient.sessionGeneration) ||
|
|
2461
|
+
!positive(recipient.processId) ||
|
|
2462
|
+
!nonempty(recipient.processBirthId) ||
|
|
2463
|
+
!nonempty(recipient.profileKey, 1024) ||
|
|
2464
|
+
!nonempty(recipient.journalBindingKey, 8192) ||
|
|
2465
|
+
recipient.profileKey === recipient.journalBindingKey ||
|
|
2466
|
+
!isRecord(recipient.target) ||
|
|
2467
|
+
!hasOnlyWireKeys(recipient.target, ["chatId", "threadId"]) ||
|
|
2468
|
+
!positive(recipient.target.chatId) ||
|
|
2469
|
+
!positive(recipient.target.threadId) ||
|
|
2470
|
+
!positive(value.operatorUserId) ||
|
|
2471
|
+
value.operatorUserId !== recipient.target.chatId ||
|
|
2472
|
+
!isRecord(executor) ||
|
|
2473
|
+
!hasOnlyWireKeys(executor, ["instanceId", "leaderEpoch"]) ||
|
|
2474
|
+
!nonempty(executor.instanceId) ||
|
|
2475
|
+
!nonempty(executor.leaderEpoch) ||
|
|
2476
|
+
!effect ||
|
|
2477
|
+
typeof value.sentAtMs !== "number" ||
|
|
2478
|
+
!Number.isFinite(value.sentAtMs) ||
|
|
2479
|
+
value.sentAtMs < 0 ||
|
|
2480
|
+
(value.auth !== undefined && typeof value.auth !== "string"))
|
|
2481
|
+
return undefined;
|
|
2482
|
+
return {
|
|
2483
|
+
kind: "follower.deliverSelectedMenu",
|
|
2484
|
+
requestId,
|
|
2485
|
+
instanceId: value.instanceId,
|
|
2486
|
+
registrationGeneration: value.registrationGeneration,
|
|
2487
|
+
operationId: value.operationId,
|
|
2488
|
+
recipient: {
|
|
2489
|
+
sessionId: recipient.sessionId,
|
|
2490
|
+
sessionGeneration: recipient.sessionGeneration,
|
|
2491
|
+
processId: recipient.processId,
|
|
2492
|
+
processBirthId: recipient.processBirthId,
|
|
2493
|
+
profileKey: recipient.profileKey,
|
|
2494
|
+
journalBindingKey: recipient.journalBindingKey,
|
|
2495
|
+
target: {
|
|
2496
|
+
chatId: recipient.target.chatId,
|
|
2497
|
+
threadId: recipient.target.threadId,
|
|
2498
|
+
},
|
|
2499
|
+
},
|
|
2500
|
+
executor: {
|
|
2501
|
+
instanceId: executor.instanceId,
|
|
2502
|
+
leaderEpoch: executor.leaderEpoch,
|
|
2503
|
+
},
|
|
2504
|
+
operatorUserId: value.operatorUserId,
|
|
2505
|
+
effect,
|
|
2506
|
+
sentAtMs: value.sentAtMs,
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
1780
2509
|
function parseCallApiEnvelope(value, requestId) {
|
|
1781
2510
|
return typeof value.instanceId === "string" &&
|
|
1782
2511
|
typeof value.method === "string" &&
|
|
@@ -1888,7 +2617,8 @@ function parseRegistration(value) {
|
|
|
1888
2617
|
if (typeof value.cwd === "string")
|
|
1889
2618
|
registration.cwd = value.cwd;
|
|
1890
2619
|
if (value.sessionId !== undefined) {
|
|
1891
|
-
if (typeof value.sessionId !== "string" ||
|
|
2620
|
+
if (typeof value.sessionId !== "string" ||
|
|
2621
|
+
!value.sessionId ||
|
|
1892
2622
|
value.sessionId !== value.sessionId.trim() ||
|
|
1893
2623
|
Buffer.byteLength(value.sessionId, "utf8") > 256)
|
|
1894
2624
|
return undefined;
|
|
@@ -1916,15 +2646,6 @@ function parseRegistration(value) {
|
|
|
1916
2646
|
registration.target = target;
|
|
1917
2647
|
return registration;
|
|
1918
2648
|
}
|
|
1919
|
-
function parseTarget(value) {
|
|
1920
|
-
if (value === undefined)
|
|
1921
|
-
return undefined;
|
|
1922
|
-
if (!isRecord(value) || typeof value.chatId !== "number")
|
|
1923
|
-
return undefined;
|
|
1924
|
-
return typeof value.threadId === "number"
|
|
1925
|
-
? { chatId: value.chatId, threadId: value.threadId }
|
|
1926
|
-
: { chatId: value.chatId };
|
|
1927
|
-
}
|
|
1928
2649
|
function parseThreadTarget(value) {
|
|
1929
2650
|
const target = parseTarget(value);
|
|
1930
2651
|
return target && typeof target.threadId === "number"
|