@llblab/pi-telegram 0.51.3 → 0.51.5
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 +2 -2
- package/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/dist/lib/bus-follower.d.ts +2 -0
- package/dist/lib/bus-follower.js +55 -2
- package/dist/lib/bus-leader.d.ts +21 -0
- package/dist/lib/bus-leader.js +144 -1
- package/dist/lib/bus.d.ts +9 -0
- package/dist/lib/bus.js +19 -0
- package/dist/lib/commands.d.ts +13 -0
- package/dist/lib/commands.js +29 -6
- package/dist/lib/extension.js +16 -0
- package/dist/lib/locks.js +5 -1
- package/dist/lib/polling.js +6 -2
- package/dist/lib/skills.d.ts +7 -1
- package/dist/lib/skills.js +32 -7
- package/dist/lib/threads.d.ts +7 -0
- package/dist/lib/threads.js +13 -5
- package/dist/package.json +3 -8
- package/docs/multi-instance-bus.md +2 -2
- package/docs/public-api.md +1 -1
- package/lib/bus-follower.ts +79 -1
- package/lib/bus-leader.ts +197 -0
- package/lib/bus.ts +33 -0
- package/lib/commands.ts +38 -6
- package/lib/extension.ts +15 -0
- package/lib/locks.ts +6 -1
- package/lib/polling.ts +10 -2
- package/lib/skills.ts +43 -7
- package/lib/threads.ts +19 -5
- package/package.json +3 -8
- package/scripts/build-dist.mjs +103 -32
package/dist/lib/skills.js
CHANGED
|
@@ -3,17 +3,42 @@
|
|
|
3
3
|
* Zones: pi agent, telegram guidance
|
|
4
4
|
* Owns source-checkout skill contribution; installed packages use their manifest
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { resolveAgentDir } from "./paths.js";
|
|
8
10
|
const TELEGRAM_SKILLS_MODULE_PATH = fileURLToPath(import.meta.url);
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
export function getTelegramExtensionPackageRoot(modulePath) {
|
|
12
|
+
let current = dirname(modulePath);
|
|
13
|
+
while (true) {
|
|
14
|
+
if (existsSync(join(current, "package.json"))) {
|
|
15
|
+
const parent = dirname(current);
|
|
16
|
+
if (basename(current) === "dist" && existsSync(join(parent, "package.json"))) {
|
|
17
|
+
return parent;
|
|
18
|
+
}
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
const parent = dirname(current);
|
|
22
|
+
if (parent === current)
|
|
23
|
+
return dirname(modulePath);
|
|
24
|
+
current = parent;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function isRawTelegramExtensionCheckout(modulePath, options = {}) {
|
|
28
|
+
const packageRoot = resolve(getTelegramExtensionPackageRoot(modulePath));
|
|
29
|
+
const agentDir = resolve(options.agentDir ?? resolveAgentDir());
|
|
30
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
31
|
+
return dirname(packageRoot) === join(agentDir, "extensions") ||
|
|
32
|
+
dirname(packageRoot) === join(cwd, ".pi", "extensions");
|
|
33
|
+
}
|
|
34
|
+
export const TELEGRAM_SKILLS_PATH = join(getTelegramExtensionPackageRoot(TELEGRAM_SKILLS_MODULE_PATH), "skills");
|
|
35
|
+
export function registerTelegramSkillDiscovery(pi, modulePath = TELEGRAM_SKILLS_MODULE_PATH, options = {}) {
|
|
36
|
+
// Pi auto-discovers extension entrypoints but not their manifest Skills.
|
|
37
|
+
// Manifest-loaded packages own Skills and filters through `pi.skills`.
|
|
38
|
+
if (!isRawTelegramExtensionCheckout(modulePath, options))
|
|
14
39
|
return false;
|
|
15
40
|
pi.on("resources_discover", () => ({
|
|
16
|
-
skillPaths: [
|
|
41
|
+
skillPaths: [join(getTelegramExtensionPackageRoot(modulePath), "skills")],
|
|
17
42
|
}));
|
|
18
43
|
return true;
|
|
19
44
|
}
|
package/dist/lib/threads.d.ts
CHANGED
|
@@ -187,6 +187,12 @@ export interface TelegramSessionReplacementIntent {
|
|
|
187
187
|
threadName?: string;
|
|
188
188
|
createdAtMs: number;
|
|
189
189
|
expiresAtMs: number;
|
|
190
|
+
/**
|
|
191
|
+
* Present only when the leader published the intent for a registered
|
|
192
|
+
* follower. Successor re-key and settlement must come from this exact
|
|
193
|
+
* follower runtime or its authenticated same-process session handoff.
|
|
194
|
+
*/
|
|
195
|
+
sourceInstanceId?: string;
|
|
190
196
|
}
|
|
191
197
|
export interface TelegramTopicTargetFile {
|
|
192
198
|
version: 1;
|
|
@@ -385,6 +391,7 @@ export declare function isTelegramLeaderSessionHandoffFresh(handoff: TelegramLea
|
|
|
385
391
|
}): handoff is TelegramLeaderSessionHandoff;
|
|
386
392
|
export declare function getTelegramThreadOwnerKey(owner: TelegramThreadOwner): string;
|
|
387
393
|
export declare function getTelegramThreadOwnerFromProfileKey(profileKey: string): TelegramThreadOwner;
|
|
394
|
+
export declare function normalizeTelegramSessionReplacementIntent(value: unknown): TelegramSessionReplacementIntent | undefined;
|
|
388
395
|
export declare function isSameTelegramProcessInstance(left: string | undefined, right: string | undefined): boolean;
|
|
389
396
|
export declare function createTelegramTopicTargetStore(options: TelegramTopicTargetStoreOptions): TelegramTopicTargetStore;
|
|
390
397
|
export declare function normalizeTelegramTopicTargetThreadName(threadName: string): string;
|
package/dist/lib/threads.js
CHANGED
|
@@ -648,7 +648,7 @@ function cloneWorkspaceBinding(binding) {
|
|
|
648
648
|
function cloneSessionReplacementIntent(intent) {
|
|
649
649
|
return { ...intent, target: { ...intent.target } };
|
|
650
650
|
}
|
|
651
|
-
function
|
|
651
|
+
export function normalizeTelegramSessionReplacementIntent(value) {
|
|
652
652
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
653
653
|
return undefined;
|
|
654
654
|
const record = value;
|
|
@@ -663,7 +663,10 @@ function normalizeSessionReplacementIntent(value) {
|
|
|
663
663
|
typeof record.messageId !== "number" || !Number.isSafeInteger(record.messageId) ||
|
|
664
664
|
typeof record.createdAtMs !== "number" || !Number.isSafeInteger(record.createdAtMs) ||
|
|
665
665
|
typeof record.expiresAtMs !== "number" || !Number.isSafeInteger(record.expiresAtMs) ||
|
|
666
|
-
record.expiresAtMs <= record.createdAtMs
|
|
666
|
+
record.expiresAtMs <= record.createdAtMs ||
|
|
667
|
+
(record.sourceInstanceId !== undefined &&
|
|
668
|
+
(typeof record.sourceInstanceId !== "string" || !record.sourceInstanceId ||
|
|
669
|
+
record.sourceInstanceId.length > 256)))
|
|
667
670
|
return undefined;
|
|
668
671
|
const continuity = record.continuity === "workspace-thread" ||
|
|
669
672
|
record.continuity === "classic-chat"
|
|
@@ -687,6 +690,8 @@ function normalizeSessionReplacementIntent(value) {
|
|
|
687
690
|
...(typeof record.threadName === "string" ? { threadName: record.threadName } : {}),
|
|
688
691
|
createdAtMs: record.createdAtMs,
|
|
689
692
|
expiresAtMs: record.expiresAtMs,
|
|
693
|
+
...(typeof record.sourceInstanceId === "string"
|
|
694
|
+
? { sourceInstanceId: record.sourceInstanceId } : {}),
|
|
690
695
|
};
|
|
691
696
|
}
|
|
692
697
|
function normalizeWorkspaceRetirementIntent(value) {
|
|
@@ -949,7 +954,7 @@ function parseTopicTargetFile(value) {
|
|
|
949
954
|
return normalized ? [normalized] : [];
|
|
950
955
|
})
|
|
951
956
|
: [],
|
|
952
|
-
sessionReplacement:
|
|
957
|
+
sessionReplacement: normalizeTelegramSessionReplacementIntent(file.sessionReplacement),
|
|
953
958
|
reservations: Array.isArray(file.reservations)
|
|
954
959
|
? file.reservations.flatMap((reservation) => {
|
|
955
960
|
const normalized = normalizeReservation(reservation);
|
|
@@ -1667,7 +1672,7 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
1667
1672
|
: undefined;
|
|
1668
1673
|
},
|
|
1669
1674
|
async commitSessionReplacementIntent(intent, isCurrent) {
|
|
1670
|
-
const next =
|
|
1675
|
+
const next = normalizeTelegramSessionReplacementIntent(intent);
|
|
1671
1676
|
if (!next || !isCurrent())
|
|
1672
1677
|
return false;
|
|
1673
1678
|
await loadFromDisk();
|
|
@@ -2081,7 +2086,10 @@ export function createTelegramTopicTargetStore(options) {
|
|
|
2081
2086
|
replacement.expiresAtMs > getNowMs() &&
|
|
2082
2087
|
replacement.profileName === (getTelegramProfile() ?? "default") &&
|
|
2083
2088
|
replacement.cwd === normalizedCwd &&
|
|
2084
|
-
replacement.sourceSessionId !== normalizedSessionId
|
|
2089
|
+
replacement.sourceSessionId !== normalizedSessionId &&
|
|
2090
|
+
(replacement.sourceInstanceId === undefined ||
|
|
2091
|
+
replacement.sourceInstanceId === instanceId ||
|
|
2092
|
+
replacement.sourceInstanceId === previousInstanceId)) {
|
|
2085
2093
|
const sourceEntry = Array.from(workspaceBindings.entries()).find(([, binding]) => binding.cwd === normalizedCwd &&
|
|
2086
2094
|
binding.sessionId === replacement.sourceSessionId &&
|
|
2087
2095
|
targetMatches(binding.target, replacement.target));
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -31,12 +31,13 @@
|
|
|
31
31
|
"test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
33
|
"build": "node scripts/build-dist.mjs",
|
|
34
|
+
"build:check": "node scripts/build-dist.mjs --check",
|
|
34
35
|
"prepack": "npm run build",
|
|
35
36
|
"check": "node -e \"await import('./dist/pi-telegram/index.js'); console.log('pi-telegram: extension import ok')\"",
|
|
36
37
|
"audit": "npm audit --omit=peer",
|
|
37
38
|
"audit:host": "npm audit",
|
|
38
39
|
"pack:check": "npm pack --dry-run",
|
|
39
|
-
"validate": "npm run build && npm run typecheck && npm test && npm run audit && npm run check && npm run pack:check"
|
|
40
|
+
"validate": "npm run build:check && npm run typecheck && npm test && npm run audit && npm run check && npm run pack:check"
|
|
40
41
|
},
|
|
41
42
|
"files": [
|
|
42
43
|
"index.ts",
|
|
@@ -102,15 +103,9 @@
|
|
|
102
103
|
"extensions": [
|
|
103
104
|
"./dist/pi-telegram/index.js"
|
|
104
105
|
],
|
|
105
|
-
"sourceExtensions": [
|
|
106
|
-
"./index.ts"
|
|
107
|
-
],
|
|
108
106
|
"skills": [
|
|
109
107
|
"./dist/skills"
|
|
110
108
|
],
|
|
111
|
-
"sourceSkills": [
|
|
112
|
-
"./skills"
|
|
113
|
-
],
|
|
114
109
|
"image": "https://raw.githubusercontent.com/llblab/pi-telegram/main/screenshot.png"
|
|
115
110
|
},
|
|
116
111
|
"peerDependencies": {
|
|
@@ -83,7 +83,7 @@ Threaded Mode meaning:
|
|
|
83
83
|
tmp/telegram/owners.json / <profile-slot> -> bus leader identity + heartbeat
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
Followers do not poll. They register with the leader and receive routed inbound updates from it. Followers still own their local queue, active-turn state, previews, final delivery planning, model switches, and Pi lifecycle. The leader owns only Telegram transport and update fanout. Pi session replacement (`new`) changes follower agent context, not bus membership: a registered follower preserves its registration and refreshes the live context instead of disconnecting. A new follower process also attempts a restore-only registration at session startup when the selected profile's local state contains
|
|
86
|
+
Followers do not poll. They register with the leader and receive routed inbound updates from it. Followers still own their local queue, active-turn state, previews, final delivery planning, model switches, and Pi lifecycle. The leader owns only Telegram transport and update fanout. Pi session replacement (`new`) changes follower agent context, not bus membership: a registered follower preserves its registration and refreshes the live context instead of disconnecting. A new follower process also attempts a restore-only registration at session startup when the selected profile's local state contains a Workspace binding for the exact `cwd` and session ID. The recent-event log distinguishes missing session bindings, disabled Threaded Mode, leader-side restore refusal, and a restore that returned without connecting; none of these refusals silently provisions a new Thread. The Telegram bus belongs to the local set of cooperating visible Pi instances rather than to the first terminal session forever: if the visible terminal leader exits, a live registered follower can take over leadership.
|
|
87
87
|
|
|
88
88
|
The assembled follower receiver admits a registration generation only after its journal-binding preparation succeeds for the same generation and current Pi context. Registration identity is available during preparation so the worker can bind, but early inbound delivery receives a negative ACK rather than writing into a retained old journal. Readiness also binds the Pi session generation, so a reused context object cannot preserve it across session replacement. A registered follower's session refresh awaits binding preparation through `setContext` without re-registering or changing its bus generation. Preparation failure keeps inbound delivery unavailable and records a diagnostic; a later refresh can retry. Normal delivery retry proceeds after successful preparation. Registration requests also retain their original Pi session generation and local attempt authority across awaits. A late reply after session drift, stop or a newer request cannot publish success or tear down newer local authority; a context refresh likewise protects its retained membership from an older pending request. This refusal does not undo remote Thread provisioning or authorize deletion.
|
|
89
89
|
|
|
@@ -226,7 +226,7 @@ Followers first try to re-register after leader reload or unknown-heartbeat resp
|
|
|
226
226
|
|
|
227
227
|
### Protocol identity and compatibility
|
|
228
228
|
|
|
229
|
-
The session-native local wire contract has protocol version `2`, independent from the npm package version. Follower registration and the leader acknowledgement carry `{ protocolVersion, runtimeBuild, capabilities }`. Capability names are canonical, unique, and sorted. A leader rejects missing or mismatched protocol identity before provisioning a target or publishing the follower into live routing; a strict follower likewise rejects an acknowledgement without compatible leader identity. Different package builds remain compatible when their protocol versions agree. `durable-follower-admission-v1` gates source forwarding, while `queue-handoff-v1` independently gates live semantic queue transfer; every participant in a routed handoff must advertise it. `workspace-follower-auto-connect-v1` gates restore-only startup admission. Session identity is part of protocol v2 rather than an optional capability. Every cwd-scoped v2 registration carries the exact bounded session ID; missing identity is rejected before provisioning or publication. Reconnect, replacement, and promotion preserve it. Protocol-v1 peers are rejected by the base version check, avoiding mixed semantic branches. `thread-display-mode-v1` gates exact-generation follower display-setting requests and Letters/Directories require compatible connected followers; returning to Names allows legacy peers. Registration checks the current display-mode requirement before provisioning and again before live publication. The leader serializes config persistence and title application. `workspace-thread-rename-v1` independently gates follower rename requests whose exact registration generation is checked before the leader mutates Telegram and persists the Workspace binding.
|
|
229
|
+
The session-native local wire contract has protocol version `2`, independent from the npm package version. Follower registration and the leader acknowledgement carry `{ protocolVersion, runtimeBuild, capabilities }`. Capability names are canonical, unique, and sorted. A leader rejects missing or mismatched protocol identity before provisioning a target or publishing the follower into live routing; a strict follower likewise rejects an acknowledgement without compatible leader identity. Different package builds remain compatible when their protocol versions agree. `durable-follower-admission-v1` gates source forwarding, while `queue-handoff-v1` independently gates live semantic queue transfer; every participant in a routed handoff must advertise it. `workspace-follower-auto-connect-v1` gates restore-only startup admission. Session identity is part of protocol v2 rather than an optional capability. Every cwd-scoped v2 registration carries the exact bounded session ID; missing identity is rejected before provisioning or publication. Reconnect, replacement, and promotion preserve it. Protocol-v1 peers are rejected by the base version check, avoiding mixed semantic branches. `thread-display-mode-v1` gates exact-generation follower display-setting requests and Letters/Directories require compatible connected followers; returning to Names allows legacy peers. Registration checks the current display-mode requirement before provisioning and again before live publication. The leader serializes config persistence and title application. `workspace-thread-rename-v1` independently gates follower rename requests whose exact registration generation is checked before the leader mutates Telegram and persists the Workspace binding. `session-replacement-intent-v1` gates Telegram `/new` from a follower Thread: followers never write `state.json`, so `follower.publishSessionReplacement` asks the leader to CAS-publish the durable intent only when the live registration generation, leader epoch, Profile, exact `cwd`, source session, Thread target, and the leader's own Workspace binding slot/name all match, with a bounded unexpired TTL. The intent records the source runtime instance; only a successor registration from that instance or its authenticated same-process handoff (`previousInstanceId`) may re-key the binding, and `follower.settleSessionReplacement` claims it once only after the leader's store shows that successor session bound to the same target. A missing capability, stale generation, or mismatch fails the command closed.
|
|
230
230
|
|
|
231
231
|
Negotiated identities remain on the live follower registry and appear in `/telegram-status --debug` plus the observational state snapshot. The Threaded Mode capability monitor owns one in-flight probe across lifecycle generations: stop/restart invalidates a late read, and a replacement monitor waits for the previous request to settle instead of creating overlapping transport transitions. Durable follower admission is authorized only when both peers advertise `durable-follower-admission-v1`, never inferred from package version: a capable runtime rejects missing support before provisioning, inbound routing, or election-roster eligibility. Authentication and exact registration generation remain mandatory independently of protocol compatibility. `follower.register` is the explicit bootstrap request and may provision; capability-gated `follower.restoreWorkspace` is startup-only and may claim, probe, or replace a remembered binding but returns `workspace-binding-unavailable` without allocating when no binding or claim-fenced legacy exact-`cwd` record exists. Both carry a fresh generation; every later request is exact-generation-fenced against the live registry entry. `bus.ack` is response-only and is rejected if submitted as a server request. Leader forwarding never synthesizes authority for an unknown recipient and preserves the follower's exact durable receipt end to end.
|
|
232
232
|
|
package/docs/public-api.md
CHANGED
|
@@ -51,7 +51,7 @@ Stable commands inside Pi:
|
|
|
51
51
|
Stable commands inside the paired Telegram DM:
|
|
52
52
|
|
|
53
53
|
- `/start` — pair when needed and open the main application menu.
|
|
54
|
-
- `/new` — after idle and empty-queue checks, request a new Pi session in the current classic chat or Thread. The bridge acknowledges the callback, deletes its confirmation, completes and removes the exact durable update, then dispatches one runtime-armed typed action through the internal `/telegram-internal` Pi gateway using `pi.sendUserMessage(..., { expandPromptTemplates: true })`. Manual invocation reports that the gateway cannot be run manually; the armed handler receives a real `ExtensionCommandContext` and calls `ctx.newSession()`; one discriminated durable intent preserves either exact classic Profile/CWD/session/chat continuity or the Thread binding with slot/name re-key. The successor CAS-claims that intent before sending one terminal result. Busy, identity-mismatch, and unavailable-host paths fail closed.
|
|
54
|
+
- `/new` — after idle and empty-queue checks, request a new Pi session in the current classic chat or Thread. The bridge acknowledges the callback, deletes its confirmation, completes and removes the exact durable update, then dispatches one runtime-armed typed action through the internal `/telegram-internal` Pi gateway using `pi.sendUserMessage(..., { expandPromptTemplates: true })`. Manual invocation reports that the gateway cannot be run manually; the armed handler receives a real `ExtensionCommandContext` and calls `ctx.newSession()`; one discriminated durable intent preserves either exact classic Profile/CWD/session/chat continuity or the Thread binding with slot/name re-key. The successor CAS-claims that intent before sending one terminal result. In a follower Thread the leader publishes and claims the intent on the follower's behalf over capability-gated, exact-generation local bus requests; followers never persist leader-owned state. Busy, identity-mismatch, stale-registration, incompatible-leader, and unavailable-host paths fail closed.
|
|
55
55
|
- `/compact` — open confirmation and compact when idle.
|
|
56
56
|
- `/next` — abort active work first when needed, attempt the interrupted prompt's abort notice, then reply `Dispatching next queued turn.` to the exact queued prompt selected for the next model turn. That dispatch notice owns the turn's one reply header; later answer messages do not reply to the same prompt again. Abort-notice failure is diagnostic and cannot block dispatch. A later `/abort` or `/stop` cancels both pending transition notices before taking ownership. The `/next` command itself is never the lifecycle-notice reply target, and aborted pending assistant text is suppressed.
|
|
57
57
|
- `/continue` — enqueue a priority `continue` prompt.
|
package/lib/bus-follower.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
49
49
|
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
50
50
|
TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
51
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
51
52
|
} from "./bus.ts";
|
|
52
53
|
import type { TelegramConfigStore, TelegramThreadDisplayMode } from "./config.ts";
|
|
53
54
|
import {
|
|
@@ -142,6 +143,11 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
|
|
|
142
143
|
target: TelegramTarget & { threadId: number },
|
|
143
144
|
) => Promise<string>;
|
|
144
145
|
setThreadDisplayMode?: (mode: TelegramThreadDisplayMode) => Promise<void>;
|
|
146
|
+
/** Leader-mediated durable publication or successor claim; true only after exact commit. */
|
|
147
|
+
requestSessionReplacement?: (
|
|
148
|
+
operation: "publish" | "settle",
|
|
149
|
+
intent: Threads.TelegramSessionReplacementIntent,
|
|
150
|
+
) => Promise<boolean>;
|
|
145
151
|
stop: () => void;
|
|
146
152
|
}
|
|
147
153
|
|
|
@@ -1919,7 +1925,13 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1919
1925
|
if (
|
|
1920
1926
|
registrationOptions?.restoreWorkspace &&
|
|
1921
1927
|
response.error?.code === "workspace-binding-unavailable"
|
|
1922
|
-
)
|
|
1928
|
+
) {
|
|
1929
|
+
deps.recordRuntimeEvent?.("bus", "Telegram follower auto-connect refused by leader: Workspace binding unavailable.", {
|
|
1930
|
+
phase: "follower-auto-connect-skip",
|
|
1931
|
+
reason: "leader-binding-unavailable",
|
|
1932
|
+
});
|
|
1933
|
+
return false;
|
|
1934
|
+
}
|
|
1923
1935
|
throw new Error(
|
|
1924
1936
|
response.message ??
|
|
1925
1937
|
"Telegram bus follower registration was rejected.",
|
|
@@ -2144,6 +2156,72 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2144
2156
|
}
|
|
2145
2157
|
return resetName;
|
|
2146
2158
|
},
|
|
2159
|
+
async requestSessionReplacement(operation, intent) {
|
|
2160
|
+
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
2161
|
+
throw new Error("Telegram follower is not registered with the leader.");
|
|
2162
|
+
}
|
|
2163
|
+
if (
|
|
2164
|
+
!hasTelegramBusCapability(
|
|
2165
|
+
deps.protocolIdentity,
|
|
2166
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
2167
|
+
) ||
|
|
2168
|
+
!hasTelegramBusCapability(
|
|
2169
|
+
deps.registrationState?.getLeaderProtocol(),
|
|
2170
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
2171
|
+
)
|
|
2172
|
+
) {
|
|
2173
|
+
throw new Error(
|
|
2174
|
+
"The active Telegram leader does not support follower session replacement. Update or restart that Pi instance.",
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
const expectedLeaderSocketPath = activeLeaderSocketPath;
|
|
2178
|
+
const expectedRegistrationGeneration = activeRegistrationGeneration;
|
|
2179
|
+
const expectedAuthSecret = activeAuthSecret;
|
|
2180
|
+
const requestId = deps.createRequestId();
|
|
2181
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
2182
|
+
socketPath: expectedLeaderSocketPath,
|
|
2183
|
+
timeoutMs: registrationTimeoutMs,
|
|
2184
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
2185
|
+
endpoint: expectedLeaderSocketPath,
|
|
2186
|
+
operation: "operation",
|
|
2187
|
+
}),
|
|
2188
|
+
envelope: {
|
|
2189
|
+
kind: operation === "publish"
|
|
2190
|
+
? "follower.publishSessionReplacement"
|
|
2191
|
+
: "follower.settleSessionReplacement",
|
|
2192
|
+
requestId,
|
|
2193
|
+
auth: expectedAuthSecret,
|
|
2194
|
+
instanceId: deps.instanceId,
|
|
2195
|
+
registrationGeneration: expectedRegistrationGeneration,
|
|
2196
|
+
intent,
|
|
2197
|
+
sentAtMs: getNowMs(),
|
|
2198
|
+
},
|
|
2199
|
+
});
|
|
2200
|
+
if (
|
|
2201
|
+
response?.kind !== "bus.ack" || !response.ok ||
|
|
2202
|
+
response.requestId !== requestId ||
|
|
2203
|
+
!isRecord(response.result) || response.result.committed !== true
|
|
2204
|
+
) {
|
|
2205
|
+
throw new Error(
|
|
2206
|
+
response?.kind === "bus.ack"
|
|
2207
|
+
? (response.message ?? "Telegram session replacement was rejected.")
|
|
2208
|
+
: "Telegram session replacement was not acknowledged.",
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
if (
|
|
2212
|
+
activeLeaderSocketPath !== expectedLeaderSocketPath ||
|
|
2213
|
+
activeRegistrationGeneration !== expectedRegistrationGeneration ||
|
|
2214
|
+
activeAuthSecret !== expectedAuthSecret ||
|
|
2215
|
+
(deps.registrationState &&
|
|
2216
|
+
deps.registrationState.getGeneration() !==
|
|
2217
|
+
expectedRegistrationGeneration)
|
|
2218
|
+
) {
|
|
2219
|
+
throw new Error(
|
|
2220
|
+
"Telegram session replacement completed for a stale follower registration.",
|
|
2221
|
+
);
|
|
2222
|
+
}
|
|
2223
|
+
return true;
|
|
2224
|
+
},
|
|
2147
2225
|
async disconnectFromLeader() {
|
|
2148
2226
|
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
2149
2227
|
return false;
|
package/lib/bus-leader.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
43
43
|
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
44
44
|
TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
45
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
45
46
|
} from "./bus.ts";
|
|
46
47
|
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
|
|
47
48
|
import type { TelegramQueueHandoffPayload } from "./queue.ts";
|
|
@@ -273,6 +274,110 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
|
273
274
|
workspaceRotation?: TelegramWorkspaceSlotRotationPorts;
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
export type TelegramBusFollowerSessionReplacementOperation = (
|
|
278
|
+
follower: TelegramBusFollowerView,
|
|
279
|
+
intent: Threads.TelegramSessionReplacementIntent,
|
|
280
|
+
isCurrent: () => boolean,
|
|
281
|
+
) => Promise<boolean>;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Leader-owned durable session-replacement authority for registered followers.
|
|
285
|
+
* Followers cannot persist `state.json`; the leader validates the request
|
|
286
|
+
* against its live registry entry and authoritative Workspace binding before
|
|
287
|
+
* one CAS publication or claim. Follower memory is never accepted as binding
|
|
288
|
+
* evidence.
|
|
289
|
+
*/
|
|
290
|
+
export function createTelegramBusFollowerSessionReplacementAuthority(deps: {
|
|
291
|
+
store: Pick<
|
|
292
|
+
Threads.TelegramTopicTargetStore,
|
|
293
|
+
| "load"
|
|
294
|
+
| "getWorkspaceBindingByTarget"
|
|
295
|
+
| "getSessionReplacementIntent"
|
|
296
|
+
| "commitSessionReplacementIntent"
|
|
297
|
+
| "removeSessionReplacementIntent"
|
|
298
|
+
>;
|
|
299
|
+
getTelegramProfile?: () => string | undefined;
|
|
300
|
+
getNowMs?: () => number;
|
|
301
|
+
ttlMs?: number;
|
|
302
|
+
}): {
|
|
303
|
+
publish: TelegramBusFollowerSessionReplacementOperation;
|
|
304
|
+
settle: TelegramBusFollowerSessionReplacementOperation;
|
|
305
|
+
} {
|
|
306
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
307
|
+
const ttlMs = deps.ttlMs ?? Threads.TELEGRAM_LEADER_SESSION_HANDOFF_TTL_MS;
|
|
308
|
+
const assertCommon = (
|
|
309
|
+
follower: TelegramBusFollowerView,
|
|
310
|
+
intent: Threads.TelegramSessionReplacementIntent,
|
|
311
|
+
): { cwd: string; sessionId: string } => {
|
|
312
|
+
const cwd = follower.cwd
|
|
313
|
+
? Threads.normalizeTelegramWorkspacePath(follower.cwd)
|
|
314
|
+
: undefined;
|
|
315
|
+
const sessionId = follower.sessionId
|
|
316
|
+
? Threads.normalizeTelegramSessionId(follower.sessionId)
|
|
317
|
+
: undefined;
|
|
318
|
+
const nowMs = getNowMs();
|
|
319
|
+
if (
|
|
320
|
+
intent.continuity !== "workspace-thread" ||
|
|
321
|
+
typeof intent.target.threadId !== "number" ||
|
|
322
|
+
follower.target?.chatId !== intent.target.chatId ||
|
|
323
|
+
follower.target.threadId !== intent.target.threadId ||
|
|
324
|
+
!cwd || cwd !== intent.cwd || !sessionId ||
|
|
325
|
+
intent.profileName !== (deps.getTelegramProfile?.() ?? "default")
|
|
326
|
+
) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
"Telegram session replacement does not match the current follower registration.",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (intent.expiresAtMs <= nowMs || intent.expiresAtMs > nowMs + ttlMs) {
|
|
332
|
+
throw new Error("Telegram session replacement intent is expired or unbounded.");
|
|
333
|
+
}
|
|
334
|
+
return { cwd, sessionId };
|
|
335
|
+
};
|
|
336
|
+
return {
|
|
337
|
+
async publish(follower, intent, isCurrent) {
|
|
338
|
+
const { sessionId } = assertCommon(follower, intent);
|
|
339
|
+
if (intent.sourceInstanceId !== follower.instanceId ||
|
|
340
|
+
intent.sourceSessionId !== sessionId) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
"Telegram session replacement source does not match the current follower registration.",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
await deps.store.load();
|
|
346
|
+
if (!isCurrent()) return false;
|
|
347
|
+
const binding = deps.store.getWorkspaceBindingByTarget(intent.target);
|
|
348
|
+
if (
|
|
349
|
+
!binding || binding.cwd !== intent.cwd ||
|
|
350
|
+
binding.sessionId !== intent.sourceSessionId ||
|
|
351
|
+
binding.slot !== intent.slot ||
|
|
352
|
+
(binding.manualThreadName ?? binding.threadName) !== intent.threadName
|
|
353
|
+
) {
|
|
354
|
+
throw new Error("Telegram session replacement binding is unavailable.");
|
|
355
|
+
}
|
|
356
|
+
return deps.store.commitSessionReplacementIntent(intent, isCurrent);
|
|
357
|
+
},
|
|
358
|
+
async settle(follower, intent, isCurrent) {
|
|
359
|
+
const { sessionId } = assertCommon(follower, intent);
|
|
360
|
+
if (
|
|
361
|
+
!intent.sourceInstanceId ||
|
|
362
|
+
(intent.sourceInstanceId !== follower.instanceId &&
|
|
363
|
+
intent.sourceInstanceId !== follower.previousInstanceId) ||
|
|
364
|
+
intent.sourceSessionId === sessionId
|
|
365
|
+
) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
"Telegram session replacement successor does not match the current follower registration.",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
await deps.store.load();
|
|
371
|
+
if (!isCurrent()) return false;
|
|
372
|
+
if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)?.cwd !==
|
|
373
|
+
intent.cwd) {
|
|
374
|
+
throw new Error("Telegram session replacement successor binding is unavailable.");
|
|
375
|
+
}
|
|
376
|
+
return deps.store.removeSessionReplacementIntent(intent, isCurrent);
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
276
381
|
export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
277
382
|
deps: TelegramBusLeaderRuntimeAssemblyDeps<TContext>,
|
|
278
383
|
): TelegramBusLeaderRuntime<TContext> & {
|
|
@@ -624,6 +729,12 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
624
729
|
}
|
|
625
730
|
});
|
|
626
731
|
});
|
|
732
|
+
const followerSessionReplacement =
|
|
733
|
+
createTelegramBusFollowerSessionReplacementAuthority({
|
|
734
|
+
store: deps.topicTargetStore,
|
|
735
|
+
getTelegramProfile: deps.getTelegramProfile,
|
|
736
|
+
getNowMs: deps.runtime.getNowMs,
|
|
737
|
+
});
|
|
627
738
|
const runtime = createTelegramBusLeaderRuntime({
|
|
628
739
|
...deps.runtime,
|
|
629
740
|
applyThreadDisplayMode,
|
|
@@ -750,6 +861,18 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
750
861
|
});
|
|
751
862
|
},
|
|
752
863
|
),
|
|
864
|
+
publishFollowerSessionReplacement: (follower, intent, isCurrent) =>
|
|
865
|
+
runWorkspaceOperation({
|
|
866
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
867
|
+
operationKind: "workspace.publish-follower-session-replacement",
|
|
868
|
+
scopes: [{ kind: "profile" }],
|
|
869
|
+
}, () => followerSessionReplacement.publish(follower, intent, isCurrent)),
|
|
870
|
+
settleFollowerSessionReplacement: (follower, intent, isCurrent) =>
|
|
871
|
+
runWorkspaceOperation({
|
|
872
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
873
|
+
operationKind: "workspace.settle-follower-session-replacement",
|
|
874
|
+
scopes: [{ kind: "profile" }],
|
|
875
|
+
}, () => followerSessionReplacement.settle(follower, intent, isCurrent)),
|
|
753
876
|
onFollowerConfirmedDead: async (follower) => {
|
|
754
877
|
await runWorkspaceOperation(
|
|
755
878
|
{
|
|
@@ -899,6 +1022,8 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
899
1022
|
resetFollowerThreadName?: (
|
|
900
1023
|
follower: TelegramBusFollowerView,
|
|
901
1024
|
) => Promise<{ threadName: string }> | { threadName: string };
|
|
1025
|
+
publishFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
1026
|
+
settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
902
1027
|
getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
|
|
903
1028
|
onFollowerRegistered?: () => void;
|
|
904
1029
|
applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
@@ -1923,6 +2048,8 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1923
2048
|
resetFollowerThreadName?: (
|
|
1924
2049
|
follower: TelegramBusFollowerView,
|
|
1925
2050
|
) => Promise<{ threadName: string }> | { threadName: string };
|
|
2051
|
+
publishFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
2052
|
+
settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
1926
2053
|
getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
|
|
1927
2054
|
onFollowerRegistered?: () => void;
|
|
1928
2055
|
applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
@@ -2547,6 +2674,74 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2547
2674
|
},
|
|
2548
2675
|
);
|
|
2549
2676
|
}
|
|
2677
|
+
case "follower.publishSessionReplacement":
|
|
2678
|
+
case "follower.settleSessionReplacement": {
|
|
2679
|
+
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
2680
|
+
return runFollowerMutation(
|
|
2681
|
+
registeredFollower ?? { instanceId: envelope.instanceId },
|
|
2682
|
+
async () => {
|
|
2683
|
+
const publish = envelope.kind === "follower.publishSessionReplacement";
|
|
2684
|
+
const operation = publish
|
|
2685
|
+
? deps.publishFollowerSessionReplacement
|
|
2686
|
+
: deps.settleFollowerSessionReplacement;
|
|
2687
|
+
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
2688
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
2689
|
+
const isCurrent = () => epoch !== undefined &&
|
|
2690
|
+
deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
2691
|
+
deps.followerRegistry.get(envelope.instanceId)?.registrationGeneration ===
|
|
2692
|
+
envelope.registrationGeneration;
|
|
2693
|
+
if (
|
|
2694
|
+
!follower?.registrationGeneration ||
|
|
2695
|
+
follower.registrationGeneration !== envelope.registrationGeneration ||
|
|
2696
|
+
!isCurrent() || !operation ||
|
|
2697
|
+
!hasTelegramBusCapability(
|
|
2698
|
+
deps.protocolIdentity,
|
|
2699
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
2700
|
+
) ||
|
|
2701
|
+
!hasTelegramBusCapability(
|
|
2702
|
+
follower.protocol,
|
|
2703
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
2704
|
+
)
|
|
2705
|
+
) {
|
|
2706
|
+
return {
|
|
2707
|
+
kind: "bus.ack" as const,
|
|
2708
|
+
requestId: envelope.requestId,
|
|
2709
|
+
ok: false,
|
|
2710
|
+
message:
|
|
2711
|
+
"Telegram session replacement requires current follower registration and compatible leader authority.",
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
try {
|
|
2715
|
+
const committed = await operation(follower, envelope.intent, isCurrent);
|
|
2716
|
+
if (!committed || !isCurrent()) {
|
|
2717
|
+
return {
|
|
2718
|
+
kind: "bus.ack" as const,
|
|
2719
|
+
requestId: envelope.requestId,
|
|
2720
|
+
ok: false,
|
|
2721
|
+
message: publish
|
|
2722
|
+
? "Telegram session replacement intent was not persisted."
|
|
2723
|
+
: "Telegram session replacement intent was not claimed.",
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
return {
|
|
2727
|
+
kind: "bus.ack" as const,
|
|
2728
|
+
requestId: envelope.requestId,
|
|
2729
|
+
ok: true,
|
|
2730
|
+
result: { committed: true },
|
|
2731
|
+
};
|
|
2732
|
+
} catch (error) {
|
|
2733
|
+
return {
|
|
2734
|
+
kind: "bus.ack" as const,
|
|
2735
|
+
requestId: envelope.requestId,
|
|
2736
|
+
ok: false,
|
|
2737
|
+
message: error instanceof Error
|
|
2738
|
+
? error.message
|
|
2739
|
+
: "Telegram session replacement request failed.",
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
},
|
|
2743
|
+
);
|
|
2744
|
+
}
|
|
2550
2745
|
case "follower.disconnect": {
|
|
2551
2746
|
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
2552
2747
|
return runFollowerMutation(
|
|
@@ -3118,6 +3313,8 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3118
3313
|
onFollowerDisconnected: deps.onFollowerDisconnected,
|
|
3119
3314
|
renameFollowerThread: deps.renameFollowerThread,
|
|
3120
3315
|
resetFollowerThreadName: deps.resetFollowerThreadName,
|
|
3316
|
+
publishFollowerSessionReplacement: deps.publishFollowerSessionReplacement,
|
|
3317
|
+
settleFollowerSessionReplacement: deps.settleFollowerSessionReplacement,
|
|
3121
3318
|
getFollowerDisplayTitle: deps.getFollowerDisplayTitle,
|
|
3122
3319
|
onFollowerRegistered: deps.onFollowerRegistered,
|
|
3123
3320
|
applyThreadDisplayMode: deps.applyThreadDisplayMode,
|
package/lib/bus.ts
CHANGED
|
@@ -52,6 +52,10 @@ import type { TelegramTarget } from "./target.ts";
|
|
|
52
52
|
import type { TelegramThreadDisplayMode } from "./config.ts";
|
|
53
53
|
import { isProcessAlive } from "./locks.ts";
|
|
54
54
|
import { resolveAgentDir } from "./paths.ts";
|
|
55
|
+
import {
|
|
56
|
+
normalizeTelegramSessionReplacementIntent,
|
|
57
|
+
type TelegramSessionReplacementIntent,
|
|
58
|
+
} from "./threads.ts";
|
|
55
59
|
|
|
56
60
|
export interface TelegramBusProcessRuntime {
|
|
57
61
|
instanceId: string;
|
|
@@ -234,6 +238,8 @@ export const TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT =
|
|
|
234
238
|
"directory-display-format-v1" as const;
|
|
235
239
|
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT =
|
|
236
240
|
"workspace-follower-auto-connect-v1" as const;
|
|
241
|
+
export const TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT =
|
|
242
|
+
"session-replacement-intent-v1" as const;
|
|
237
243
|
|
|
238
244
|
export interface TelegramBusProtocolIdentity {
|
|
239
245
|
protocolVersion: number;
|
|
@@ -816,6 +822,16 @@ export type TelegramBusEnvelope = (
|
|
|
816
822
|
target: TelegramTarget & { threadId: number };
|
|
817
823
|
sentAtMs: number;
|
|
818
824
|
}
|
|
825
|
+
| {
|
|
826
|
+
kind:
|
|
827
|
+
| "follower.publishSessionReplacement"
|
|
828
|
+
| "follower.settleSessionReplacement";
|
|
829
|
+
requestId: string;
|
|
830
|
+
instanceId: string;
|
|
831
|
+
registrationGeneration: string;
|
|
832
|
+
intent: TelegramSessionReplacementIntent;
|
|
833
|
+
sentAtMs: number;
|
|
834
|
+
}
|
|
819
835
|
| {
|
|
820
836
|
kind: "leader.forwardCallback";
|
|
821
837
|
requestId: string;
|
|
@@ -1055,6 +1071,23 @@ export function parseTelegramBusEnvelope(
|
|
|
1055
1071
|
}
|
|
1056
1072
|
break;
|
|
1057
1073
|
}
|
|
1074
|
+
case "follower.publishSessionReplacement":
|
|
1075
|
+
case "follower.settleSessionReplacement": {
|
|
1076
|
+
const intent = normalizeTelegramSessionReplacementIntent(value.intent);
|
|
1077
|
+
if (typeof value.instanceId === "string" &&
|
|
1078
|
+
typeof value.registrationGeneration === "string" &&
|
|
1079
|
+
typeof value.sentAtMs === "number" && intent) {
|
|
1080
|
+
envelope = {
|
|
1081
|
+
kind,
|
|
1082
|
+
requestId,
|
|
1083
|
+
instanceId: value.instanceId,
|
|
1084
|
+
registrationGeneration: value.registrationGeneration,
|
|
1085
|
+
intent,
|
|
1086
|
+
sentAtMs: value.sentAtMs,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1058
1091
|
case "leader.forwardCallback":
|
|
1059
1092
|
envelope = parseForwardCallbackEnvelope(value, requestId);
|
|
1060
1093
|
break;
|