@llblab/pi-kit 0.24.0 → 0.24.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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +5 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +55 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +144 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +19 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +13 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +29 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +16 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +5 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +7 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +13 -5
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +79 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +197 -0
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +33 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +38 -6
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +15 -0
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +6 -1
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +10 -2
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +19 -5
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.24.1 - 2026-09-26
|
|
6
|
+
|
|
7
|
+
- `Follower Thread Hotfix`: Advances the exact Telegram pin to `0.51.5`. Telegram `/new` in a follower Thread now replaces that follower's session while the leader durably authorizes the Thread binding handoff; automatic follower restore reports why it could not reconnect. Package membership, load order, and other pins remain unchanged.
|
|
8
|
+
|
|
5
9
|
## 0.24.0 - 2026-09-25
|
|
6
10
|
|
|
7
11
|
- `Minimal State Flow Reconciliation`: Advances the exact State Flow pin to `0.21.0`. Frozen context heads retain cache-stable prefixes while predictable accepted writes omit redundant semantic tails. Shared drift, changed hints and unknown fallback still reconcile; lazy bodies and provenance remain hidden. Artifact prediction cannot reject an already accepted patch. Canonical storage, other package pins, resources and load order are unchanged.
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
17
17
|
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.10.0` | Compact Codex/Spark subscription-limit and Business credit-usage status |
|
|
18
18
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.8.2` | Visible continuation scheduling and bounded worker Skills through compiled, manifest-owned resources |
|
|
19
19
|
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.21.0` | Incremental scoped context/memory compiler with cache-stable heads, sparse acceptance reconciliation, lazy isolation, private fork memory, and optional Git backup |
|
|
20
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.51.
|
|
20
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.51.5` | Telegram companion with follower Thread `/new`, restore diagnostics, filterable Skills, files, voice, and controls |
|
|
21
21
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.15.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
22
22
|
|
|
23
23
|
Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
|
|
@@ -125,7 +125,7 @@ The detailed map is canonical in [`docs/architecture.md`](./docs/architecture.md
|
|
|
125
125
|
- Command templates remain compact and shell-free. Use string leaves or ordered `template` arrays; shell operators are not an execution contract. Examples use portable executable placeholders, never machine-local paths.
|
|
126
126
|
- `telegram_attach` is the canonical file path and `telegram_message` the direct Markdown text/buttons path. Both require current direct or registered-follower authority and must not replace the normal active-turn reply.
|
|
127
127
|
- Inbound handlers transform text/media before queueing; outbound handlers precede programmatic/provider fallbacks. Public contracts and ordering live in `docs/inbound.md`, `docs/outbound.md`, and `docs/public-api.md`.
|
|
128
|
-
- Pi integration uses public hooks and APIs. Telegram `/new` is scheduled against the exact durable update, dispatched only after that update is removed from the journal, and then routed through the single `/telegram-internal` Pi gateway via `pi.sendUserMessage(..., { expandPromptTemplates: true })`; only a runtime-armed typed action may execute, manual invocation reports that the command cannot be run manually, and the handler receives the real `ExtensionCommandContext` before calling `ctx.newSession()`. Before replacement, CAS-publish one exact expiring handoff in the profile target snapshot. `workspace-thread` successors re-key the matching Workspace binding; `classic-chat` successors preserve Profile/CWD/session/chat continuity without creating a binding or invoking topic APIs. Both atomically claim the intent before one terminal result, and the old `withSession` path never publishes the same success. Never replace the session while inbound authority is unsettled, store stale command contexts, accept an expired or mismatched handoff, inject terminal input, spawn a shadow Pi process, or mutate session files.
|
|
128
|
+
- Pi integration uses public hooks and APIs. Telegram `/new` is scheduled against the exact durable update, dispatched only after that update is removed from the journal, and then routed through the single `/telegram-internal` Pi gateway via `pi.sendUserMessage(..., { expandPromptTemplates: true })`; only a runtime-armed typed action may execute, manual invocation reports that the command cannot be run manually, and the handler receives the real `ExtensionCommandContext` before calling `ctx.newSession()`. Before replacement, CAS-publish one exact expiring handoff in the profile target snapshot. `workspace-thread` successors re-key the matching Workspace binding; `classic-chat` successors preserve Profile/CWD/session/chat continuity without creating a binding or invoking topic APIs. Both atomically claim the intent before one terminal result, and the old `withSession` path never publishes the same success. A registered follower cannot persist leader-owned state: its Workspace Thread intent is published and claimed by the leader over capability-gated exact-generation bus RPC after leader-side binding validation, and records its source runtime instance so only that follower lineage may re-key or claim it. Never replace the session while inbound authority is unsettled, store stale command contexts, accept an expired or mismatched handoff, inject terminal input, spawn a shadow Pi process, or mutate session files.
|
|
129
129
|
|
|
130
130
|
## 7. Engineering Conventions
|
|
131
131
|
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.51.5: Follower Thread new-session hotfix
|
|
8
|
+
|
|
9
|
+
- `Follower Thread /new`: Telegram `/new` now starts a new session in a follower's Pi process and preserves its Thread binding. The leader publishes and later claims the durable replacement intent through capability-gated, generation-fenced bus requests, validating its own live registration and Workspace binding before accepting the follower's request. Incompatible or stale leaders fail closed rather than silently switching sessions.
|
|
10
|
+
- `Follower restore diagnostics`: Session startup now records when automatic follower restore lacks a session binding, Threaded Mode, or leader-side Workspace authority, or finishes without connecting. Restore-only refusal still cannot create a new Thread; `/telegram-connect` remains the explicit recovery path.
|
|
11
|
+
|
|
7
12
|
## 0.51.4: Resolver-owned Skills and drift-safe Git installs
|
|
8
13
|
|
|
9
14
|
- `Resolver-owned Skills`: Auto-discovered user/project checkouts now contribute source Skills even when Pi selects their compiled entrypoint, while manifest-loaded npm, Git, and Pi Kit packages retain `pi.skills` filters and package provenance. Resolver regressions cover checkout and filtered-package behavior; unsupported source manifest aliases are removed.
|
|
@@ -54,6 +54,8 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
|
|
|
54
54
|
threadId: number;
|
|
55
55
|
}) => Promise<string>;
|
|
56
56
|
setThreadDisplayMode?: (mode: TelegramThreadDisplayMode) => Promise<void>;
|
|
57
|
+
/** Leader-mediated durable publication or successor claim; true only after exact commit. */
|
|
58
|
+
requestSessionReplacement?: (operation: "publish" | "settle", intent: Threads.TelegramSessionReplacementIntent) => Promise<boolean>;
|
|
57
59
|
stop: () => void;
|
|
58
60
|
}
|
|
59
61
|
export interface TelegramBusFollowerSessionReplacementSuspenderDeps {
|
|
@@ -11,7 +11,7 @@ import * as Threads from "./threads.js";
|
|
|
11
11
|
import { parseTelegramUpdateJournalQueueOwner } from "./journal.js";
|
|
12
12
|
import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
|
|
13
13
|
import { isTelegramApiMethodRetrySafe, TelegramApiCommitUnknownError, TelegramApiStaleTargetError, } from "./telegram-api.js";
|
|
14
|
-
import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusFollowerTargetController, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, } from "./bus.js";
|
|
14
|
+
import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusFollowerTargetController, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, } from "./bus.js";
|
|
15
15
|
import { getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_REGISTRATION_RETRY, } from "./bus-transport.js";
|
|
16
16
|
import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
17
17
|
export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
|
|
@@ -1214,8 +1214,13 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1214
1214
|
deps.setActiveAuthSecret?.(undefined);
|
|
1215
1215
|
await deps.stopReceiving?.();
|
|
1216
1216
|
if (registrationOptions?.restoreWorkspace &&
|
|
1217
|
-
response.error?.code === "workspace-binding-unavailable")
|
|
1217
|
+
response.error?.code === "workspace-binding-unavailable") {
|
|
1218
|
+
deps.recordRuntimeEvent?.("bus", "Telegram follower auto-connect refused by leader: Workspace binding unavailable.", {
|
|
1219
|
+
phase: "follower-auto-connect-skip",
|
|
1220
|
+
reason: "leader-binding-unavailable",
|
|
1221
|
+
});
|
|
1218
1222
|
return false;
|
|
1223
|
+
}
|
|
1219
1224
|
throw new Error(response.message ??
|
|
1220
1225
|
"Telegram bus follower registration was rejected.");
|
|
1221
1226
|
}
|
|
@@ -1426,6 +1431,54 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
|
|
|
1426
1431
|
}
|
|
1427
1432
|
return resetName;
|
|
1428
1433
|
},
|
|
1434
|
+
async requestSessionReplacement(operation, intent) {
|
|
1435
|
+
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
1436
|
+
throw new Error("Telegram follower is not registered with the leader.");
|
|
1437
|
+
}
|
|
1438
|
+
if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT) ||
|
|
1439
|
+
!hasTelegramBusCapability(deps.registrationState?.getLeaderProtocol(), TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT)) {
|
|
1440
|
+
throw new Error("The active Telegram leader does not support follower session replacement. Update or restart that Pi instance.");
|
|
1441
|
+
}
|
|
1442
|
+
const expectedLeaderSocketPath = activeLeaderSocketPath;
|
|
1443
|
+
const expectedRegistrationGeneration = activeRegistrationGeneration;
|
|
1444
|
+
const expectedAuthSecret = activeAuthSecret;
|
|
1445
|
+
const requestId = deps.createRequestId();
|
|
1446
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
1447
|
+
socketPath: expectedLeaderSocketPath,
|
|
1448
|
+
timeoutMs: registrationTimeoutMs,
|
|
1449
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
1450
|
+
endpoint: expectedLeaderSocketPath,
|
|
1451
|
+
operation: "operation",
|
|
1452
|
+
}),
|
|
1453
|
+
envelope: {
|
|
1454
|
+
kind: operation === "publish"
|
|
1455
|
+
? "follower.publishSessionReplacement"
|
|
1456
|
+
: "follower.settleSessionReplacement",
|
|
1457
|
+
requestId,
|
|
1458
|
+
auth: expectedAuthSecret,
|
|
1459
|
+
instanceId: deps.instanceId,
|
|
1460
|
+
registrationGeneration: expectedRegistrationGeneration,
|
|
1461
|
+
intent,
|
|
1462
|
+
sentAtMs: getNowMs(),
|
|
1463
|
+
},
|
|
1464
|
+
});
|
|
1465
|
+
if (response?.kind !== "bus.ack" || !response.ok ||
|
|
1466
|
+
response.requestId !== requestId ||
|
|
1467
|
+
!isRecord(response.result) || response.result.committed !== true) {
|
|
1468
|
+
throw new Error(response?.kind === "bus.ack"
|
|
1469
|
+
? (response.message ?? "Telegram session replacement was rejected.")
|
|
1470
|
+
: "Telegram session replacement was not acknowledged.");
|
|
1471
|
+
}
|
|
1472
|
+
if (activeLeaderSocketPath !== expectedLeaderSocketPath ||
|
|
1473
|
+
activeRegistrationGeneration !== expectedRegistrationGeneration ||
|
|
1474
|
+
activeAuthSecret !== expectedAuthSecret ||
|
|
1475
|
+
(deps.registrationState &&
|
|
1476
|
+
deps.registrationState.getGeneration() !==
|
|
1477
|
+
expectedRegistrationGeneration)) {
|
|
1478
|
+
throw new Error("Telegram session replacement completed for a stale follower registration.");
|
|
1479
|
+
}
|
|
1480
|
+
return true;
|
|
1481
|
+
},
|
|
1429
1482
|
async disconnectFromLeader() {
|
|
1430
1483
|
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
1431
1484
|
return false;
|
|
@@ -135,6 +135,23 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
|
135
135
|
runWorkspaceOperation?: TelegramBusWorkspaceAdmissionRunner;
|
|
136
136
|
workspaceRotation?: TelegramWorkspaceSlotRotationPorts;
|
|
137
137
|
}
|
|
138
|
+
export type TelegramBusFollowerSessionReplacementOperation = (follower: TelegramBusFollowerView, intent: Threads.TelegramSessionReplacementIntent, isCurrent: () => boolean) => Promise<boolean>;
|
|
139
|
+
/**
|
|
140
|
+
* Leader-owned durable session-replacement authority for registered followers.
|
|
141
|
+
* Followers cannot persist `state.json`; the leader validates the request
|
|
142
|
+
* against its live registry entry and authoritative Workspace binding before
|
|
143
|
+
* one CAS publication or claim. Follower memory is never accepted as binding
|
|
144
|
+
* evidence.
|
|
145
|
+
*/
|
|
146
|
+
export declare function createTelegramBusFollowerSessionReplacementAuthority(deps: {
|
|
147
|
+
store: Pick<Threads.TelegramTopicTargetStore, "load" | "getWorkspaceBindingByTarget" | "getSessionReplacementIntent" | "commitSessionReplacementIntent" | "removeSessionReplacementIntent">;
|
|
148
|
+
getTelegramProfile?: () => string | undefined;
|
|
149
|
+
getNowMs?: () => number;
|
|
150
|
+
ttlMs?: number;
|
|
151
|
+
}): {
|
|
152
|
+
publish: TelegramBusFollowerSessionReplacementOperation;
|
|
153
|
+
settle: TelegramBusFollowerSessionReplacementOperation;
|
|
154
|
+
};
|
|
138
155
|
export declare function createTelegramBusLeaderRuntimeAssembly<TContext>(deps: TelegramBusLeaderRuntimeAssemblyDeps<TContext>): TelegramBusLeaderRuntime<TContext> & {
|
|
139
156
|
renameLeaderThreadAdmitted: (threadName: string, expectedTarget?: Threads.TelegramTopicTargetRecord["target"]) => Promise<Threads.TelegramTopicTargetRecord>;
|
|
140
157
|
resetLeaderThreadName: (expectedTarget: Threads.TelegramTopicTargetRecord["target"]) => Promise<{
|
|
@@ -188,6 +205,8 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
188
205
|
}> | {
|
|
189
206
|
threadName: string;
|
|
190
207
|
};
|
|
208
|
+
publishFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
209
|
+
settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
191
210
|
getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
|
|
192
211
|
onFollowerRegistered?: () => void;
|
|
193
212
|
applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
@@ -273,6 +292,8 @@ export declare function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
273
292
|
}> | {
|
|
274
293
|
threadName: string;
|
|
275
294
|
};
|
|
295
|
+
publishFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
296
|
+
settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
276
297
|
getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
|
|
277
298
|
onFollowerRegistered?: () => void;
|
|
278
299
|
applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
@@ -9,7 +9,7 @@ import { createTelegramThreadDisplayReconciler, resolveTelegramInitialWorkspaceD
|
|
|
9
9
|
import * as ThreadReconciler from "./thread-reconciler.js";
|
|
10
10
|
import { getTelegramApiErrorRequestTarget, isTelegramApiCommitUnknownError, } from "./telegram-api.js";
|
|
11
11
|
import * as Threads from "./threads.js";
|
|
12
|
-
import { createTelegramBusLocalServer, createUnauthorizedBusAck, getTelegramBusEnvelopeTrafficClass, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, getTelegramBusFollowerSocketPath, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, } from "./bus.js";
|
|
12
|
+
import { createTelegramBusLocalServer, createUnauthorizedBusAck, getTelegramBusEnvelopeTrafficClass, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, getTelegramBusFollowerSocketPath, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, } from "./bus.js";
|
|
13
13
|
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.js";
|
|
14
14
|
import { createTelegramWorkspaceOperationRuntime, createTelegramWorkspaceSlotRotation, } from "./workspace-retirement.js";
|
|
15
15
|
import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
@@ -27,6 +27,75 @@ function formatTelegramBusInstanceLabel(input) {
|
|
|
27
27
|
return escapeHtml(threadName);
|
|
28
28
|
return input.slot && /^[A-Z]$/.test(input.slot) ? input.slot : "?";
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Leader-owned durable session-replacement authority for registered followers.
|
|
32
|
+
* Followers cannot persist `state.json`; the leader validates the request
|
|
33
|
+
* against its live registry entry and authoritative Workspace binding before
|
|
34
|
+
* one CAS publication or claim. Follower memory is never accepted as binding
|
|
35
|
+
* evidence.
|
|
36
|
+
*/
|
|
37
|
+
export function createTelegramBusFollowerSessionReplacementAuthority(deps) {
|
|
38
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
39
|
+
const ttlMs = deps.ttlMs ?? Threads.TELEGRAM_LEADER_SESSION_HANDOFF_TTL_MS;
|
|
40
|
+
const assertCommon = (follower, intent) => {
|
|
41
|
+
const cwd = follower.cwd
|
|
42
|
+
? Threads.normalizeTelegramWorkspacePath(follower.cwd)
|
|
43
|
+
: undefined;
|
|
44
|
+
const sessionId = follower.sessionId
|
|
45
|
+
? Threads.normalizeTelegramSessionId(follower.sessionId)
|
|
46
|
+
: undefined;
|
|
47
|
+
const nowMs = getNowMs();
|
|
48
|
+
if (intent.continuity !== "workspace-thread" ||
|
|
49
|
+
typeof intent.target.threadId !== "number" ||
|
|
50
|
+
follower.target?.chatId !== intent.target.chatId ||
|
|
51
|
+
follower.target.threadId !== intent.target.threadId ||
|
|
52
|
+
!cwd || cwd !== intent.cwd || !sessionId ||
|
|
53
|
+
intent.profileName !== (deps.getTelegramProfile?.() ?? "default")) {
|
|
54
|
+
throw new Error("Telegram session replacement does not match the current follower registration.");
|
|
55
|
+
}
|
|
56
|
+
if (intent.expiresAtMs <= nowMs || intent.expiresAtMs > nowMs + ttlMs) {
|
|
57
|
+
throw new Error("Telegram session replacement intent is expired or unbounded.");
|
|
58
|
+
}
|
|
59
|
+
return { cwd, sessionId };
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
async publish(follower, intent, isCurrent) {
|
|
63
|
+
const { sessionId } = assertCommon(follower, intent);
|
|
64
|
+
if (intent.sourceInstanceId !== follower.instanceId ||
|
|
65
|
+
intent.sourceSessionId !== sessionId) {
|
|
66
|
+
throw new Error("Telegram session replacement source does not match the current follower registration.");
|
|
67
|
+
}
|
|
68
|
+
await deps.store.load();
|
|
69
|
+
if (!isCurrent())
|
|
70
|
+
return false;
|
|
71
|
+
const binding = deps.store.getWorkspaceBindingByTarget(intent.target);
|
|
72
|
+
if (!binding || binding.cwd !== intent.cwd ||
|
|
73
|
+
binding.sessionId !== intent.sourceSessionId ||
|
|
74
|
+
binding.slot !== intent.slot ||
|
|
75
|
+
(binding.manualThreadName ?? binding.threadName) !== intent.threadName) {
|
|
76
|
+
throw new Error("Telegram session replacement binding is unavailable.");
|
|
77
|
+
}
|
|
78
|
+
return deps.store.commitSessionReplacementIntent(intent, isCurrent);
|
|
79
|
+
},
|
|
80
|
+
async settle(follower, intent, isCurrent) {
|
|
81
|
+
const { sessionId } = assertCommon(follower, intent);
|
|
82
|
+
if (!intent.sourceInstanceId ||
|
|
83
|
+
(intent.sourceInstanceId !== follower.instanceId &&
|
|
84
|
+
intent.sourceInstanceId !== follower.previousInstanceId) ||
|
|
85
|
+
intent.sourceSessionId === sessionId) {
|
|
86
|
+
throw new Error("Telegram session replacement successor does not match the current follower registration.");
|
|
87
|
+
}
|
|
88
|
+
await deps.store.load();
|
|
89
|
+
if (!isCurrent())
|
|
90
|
+
return false;
|
|
91
|
+
if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)?.cwd !==
|
|
92
|
+
intent.cwd) {
|
|
93
|
+
throw new Error("Telegram session replacement successor binding is unavailable.");
|
|
94
|
+
}
|
|
95
|
+
return deps.store.removeSessionReplacementIntent(intent, isCurrent);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
30
99
|
export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
31
100
|
const runWorkspaceAdmission = deps.getWorkspaceAdmission
|
|
32
101
|
? (input, operation) => {
|
|
@@ -327,6 +396,11 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
327
396
|
}
|
|
328
397
|
});
|
|
329
398
|
});
|
|
399
|
+
const followerSessionReplacement = createTelegramBusFollowerSessionReplacementAuthority({
|
|
400
|
+
store: deps.topicTargetStore,
|
|
401
|
+
getTelegramProfile: deps.getTelegramProfile,
|
|
402
|
+
getNowMs: deps.runtime.getNowMs,
|
|
403
|
+
});
|
|
330
404
|
const runtime = createTelegramBusLeaderRuntime({
|
|
331
405
|
...deps.runtime,
|
|
332
406
|
applyThreadDisplayMode,
|
|
@@ -427,6 +501,16 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
|
|
|
427
501
|
}
|
|
428
502
|
});
|
|
429
503
|
}),
|
|
504
|
+
publishFollowerSessionReplacement: (follower, intent, isCurrent) => runWorkspaceOperation({
|
|
505
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
506
|
+
operationKind: "workspace.publish-follower-session-replacement",
|
|
507
|
+
scopes: [{ kind: "profile" }],
|
|
508
|
+
}, () => followerSessionReplacement.publish(follower, intent, isCurrent)),
|
|
509
|
+
settleFollowerSessionReplacement: (follower, intent, isCurrent) => runWorkspaceOperation({
|
|
510
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
511
|
+
operationKind: "workspace.settle-follower-session-replacement",
|
|
512
|
+
scopes: [{ kind: "profile" }],
|
|
513
|
+
}, () => followerSessionReplacement.settle(follower, intent, isCurrent)),
|
|
430
514
|
onFollowerConfirmedDead: async (follower) => {
|
|
431
515
|
await runWorkspaceOperation({
|
|
432
516
|
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
@@ -1796,6 +1880,63 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
|
|
|
1796
1880
|
}
|
|
1797
1881
|
});
|
|
1798
1882
|
}
|
|
1883
|
+
case "follower.publishSessionReplacement":
|
|
1884
|
+
case "follower.settleSessionReplacement": {
|
|
1885
|
+
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
1886
|
+
return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
|
|
1887
|
+
const publish = envelope.kind === "follower.publishSessionReplacement";
|
|
1888
|
+
const operation = publish
|
|
1889
|
+
? deps.publishFollowerSessionReplacement
|
|
1890
|
+
: deps.settleFollowerSessionReplacement;
|
|
1891
|
+
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
1892
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
1893
|
+
const isCurrent = () => epoch !== undefined &&
|
|
1894
|
+
deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
1895
|
+
deps.followerRegistry.get(envelope.instanceId)?.registrationGeneration ===
|
|
1896
|
+
envelope.registrationGeneration;
|
|
1897
|
+
if (!follower?.registrationGeneration ||
|
|
1898
|
+
follower.registrationGeneration !== envelope.registrationGeneration ||
|
|
1899
|
+
!isCurrent() || !operation ||
|
|
1900
|
+
!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT) ||
|
|
1901
|
+
!hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT)) {
|
|
1902
|
+
return {
|
|
1903
|
+
kind: "bus.ack",
|
|
1904
|
+
requestId: envelope.requestId,
|
|
1905
|
+
ok: false,
|
|
1906
|
+
message: "Telegram session replacement requires current follower registration and compatible leader authority.",
|
|
1907
|
+
};
|
|
1908
|
+
}
|
|
1909
|
+
try {
|
|
1910
|
+
const committed = await operation(follower, envelope.intent, isCurrent);
|
|
1911
|
+
if (!committed || !isCurrent()) {
|
|
1912
|
+
return {
|
|
1913
|
+
kind: "bus.ack",
|
|
1914
|
+
requestId: envelope.requestId,
|
|
1915
|
+
ok: false,
|
|
1916
|
+
message: publish
|
|
1917
|
+
? "Telegram session replacement intent was not persisted."
|
|
1918
|
+
: "Telegram session replacement intent was not claimed.",
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
return {
|
|
1922
|
+
kind: "bus.ack",
|
|
1923
|
+
requestId: envelope.requestId,
|
|
1924
|
+
ok: true,
|
|
1925
|
+
result: { committed: true },
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
catch (error) {
|
|
1929
|
+
return {
|
|
1930
|
+
kind: "bus.ack",
|
|
1931
|
+
requestId: envelope.requestId,
|
|
1932
|
+
ok: false,
|
|
1933
|
+
message: error instanceof Error
|
|
1934
|
+
? error.message
|
|
1935
|
+
: "Telegram session replacement request failed.",
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1799
1940
|
case "follower.disconnect": {
|
|
1800
1941
|
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
1801
1942
|
return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
|
|
@@ -2303,6 +2444,8 @@ export function createTelegramBusLeaderRuntime(deps) {
|
|
|
2303
2444
|
onFollowerDisconnected: deps.onFollowerDisconnected,
|
|
2304
2445
|
renameFollowerThread: deps.renameFollowerThread,
|
|
2305
2446
|
resetFollowerThreadName: deps.resetFollowerThreadName,
|
|
2447
|
+
publishFollowerSessionReplacement: deps.publishFollowerSessionReplacement,
|
|
2448
|
+
settleFollowerSessionReplacement: deps.settleFollowerSessionReplacement,
|
|
2306
2449
|
getFollowerDisplayTitle: deps.getFollowerDisplayTitle,
|
|
2307
2450
|
onFollowerRegistered: deps.onFollowerRegistered,
|
|
2308
2451
|
applyThreadDisplayMode: deps.applyThreadDisplayMode,
|
|
@@ -8,6 +8,7 @@ import { type TelegramBusTransportEventRecorder, type TelegramBusTransportRetryP
|
|
|
8
8
|
import { type TelegramQueueHandoffPayload } from "./queue.ts";
|
|
9
9
|
import type { TelegramTarget } from "./target.ts";
|
|
10
10
|
import type { TelegramThreadDisplayMode } from "./config.ts";
|
|
11
|
+
import { type TelegramSessionReplacementIntent } from "./threads.ts";
|
|
11
12
|
export interface TelegramBusProcessRuntime {
|
|
12
13
|
instanceId: string;
|
|
13
14
|
processId: number;
|
|
@@ -60,6 +61,7 @@ export declare const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME: "workspace
|
|
|
60
61
|
export declare const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE: "thread-display-mode-v1";
|
|
61
62
|
export declare const TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT: "directory-display-format-v1";
|
|
62
63
|
export declare const TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT: "workspace-follower-auto-connect-v1";
|
|
64
|
+
export declare const TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT: "session-replacement-intent-v1";
|
|
63
65
|
export interface TelegramBusProtocolIdentity {
|
|
64
66
|
protocolVersion: number;
|
|
65
67
|
runtimeBuild: string;
|
|
@@ -249,6 +251,13 @@ export type TelegramBusEnvelope = ({
|
|
|
249
251
|
threadId: number;
|
|
250
252
|
};
|
|
251
253
|
sentAtMs: number;
|
|
254
|
+
} | {
|
|
255
|
+
kind: "follower.publishSessionReplacement" | "follower.settleSessionReplacement";
|
|
256
|
+
requestId: string;
|
|
257
|
+
instanceId: string;
|
|
258
|
+
registrationGeneration: string;
|
|
259
|
+
intent: TelegramSessionReplacementIntent;
|
|
260
|
+
sentAtMs: number;
|
|
252
261
|
} | {
|
|
253
262
|
kind: "leader.forwardCallback";
|
|
254
263
|
requestId: string;
|
|
@@ -15,6 +15,7 @@ import { classifyTelegramBusTransportError, createTelegramBusTransportTimeoutErr
|
|
|
15
15
|
import { TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS, TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES, } from "./queue.js";
|
|
16
16
|
import { isProcessAlive } from "./locks.js";
|
|
17
17
|
import { resolveAgentDir } from "./paths.js";
|
|
18
|
+
import { normalizeTelegramSessionReplacementIntent, } from "./threads.js";
|
|
18
19
|
function readDarwinProcessStart(pid) {
|
|
19
20
|
return execFileSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
20
21
|
encoding: "utf8",
|
|
@@ -124,6 +125,7 @@ export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME = "workspace-thread
|
|
|
124
125
|
export const TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE = "thread-display-mode-v1";
|
|
125
126
|
export const TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT = "directory-display-format-v1";
|
|
126
127
|
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT = "workspace-follower-auto-connect-v1";
|
|
128
|
+
export const TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT = "session-replacement-intent-v1";
|
|
127
129
|
export function createTelegramBusProtocolIdentity(input) {
|
|
128
130
|
const runtimeBuild = input.runtimeBuild.trim();
|
|
129
131
|
if (!runtimeBuild || runtimeBuild.length > 128) {
|
|
@@ -505,6 +507,23 @@ export function parseTelegramBusEnvelope(line) {
|
|
|
505
507
|
}
|
|
506
508
|
break;
|
|
507
509
|
}
|
|
510
|
+
case "follower.publishSessionReplacement":
|
|
511
|
+
case "follower.settleSessionReplacement": {
|
|
512
|
+
const intent = normalizeTelegramSessionReplacementIntent(value.intent);
|
|
513
|
+
if (typeof value.instanceId === "string" &&
|
|
514
|
+
typeof value.registrationGeneration === "string" &&
|
|
515
|
+
typeof value.sentAtMs === "number" && intent) {
|
|
516
|
+
envelope = {
|
|
517
|
+
kind,
|
|
518
|
+
requestId,
|
|
519
|
+
instanceId: value.instanceId,
|
|
520
|
+
registrationGeneration: value.registrationGeneration,
|
|
521
|
+
intent,
|
|
522
|
+
sentAtMs: value.sentAtMs,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
508
527
|
case "leader.forwardCallback":
|
|
509
528
|
envelope = parseForwardCallbackEnvelope(value, requestId);
|
|
510
529
|
break;
|
|
@@ -645,6 +645,19 @@ export interface TelegramSessionActionAssemblyDeps {
|
|
|
645
645
|
};
|
|
646
646
|
getProfileName: () => string | undefined;
|
|
647
647
|
ownsPersistence: () => boolean;
|
|
648
|
+
/**
|
|
649
|
+
* Registered-follower port. A follower cannot persist leader-owned state, so
|
|
650
|
+
* its Workspace Thread intent is published and claimed by the leader over
|
|
651
|
+
* authenticated generation-fenced bus RPC.
|
|
652
|
+
*/
|
|
653
|
+
follower?: {
|
|
654
|
+
instanceId: string;
|
|
655
|
+
isRegisteredFor: (target: {
|
|
656
|
+
chatId: number;
|
|
657
|
+
threadId?: number;
|
|
658
|
+
}) => boolean;
|
|
659
|
+
requestSessionReplacement: (operation: "publish" | "settle", intent: TelegramSessionReplacementIntent) => Promise<boolean>;
|
|
660
|
+
};
|
|
648
661
|
sendResult: (target: {
|
|
649
662
|
chatId: number;
|
|
650
663
|
threadId?: number;
|
|
@@ -1294,7 +1294,15 @@ export function createTelegramSessionActionAssembly(deps) {
|
|
|
1294
1294
|
sendUserMessage: deps.sendUserMessage,
|
|
1295
1295
|
notifyResult(target, result) { return sendTerminalResult(target, result); },
|
|
1296
1296
|
async prepareReplacement(ctx, updateId, target) {
|
|
1297
|
-
|
|
1297
|
+
const follower = !deps.ownsPersistence() && typeof target.threadId === "number" &&
|
|
1298
|
+
deps.follower?.isRegisteredFor(target)
|
|
1299
|
+
? deps.follower
|
|
1300
|
+
: undefined;
|
|
1301
|
+
// Follower memory is not authority; reread the leader-published snapshot.
|
|
1302
|
+
if (follower && deps.store.refresh)
|
|
1303
|
+
await deps.store.refresh();
|
|
1304
|
+
else
|
|
1305
|
+
await deps.store.load();
|
|
1298
1306
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
1299
1307
|
const binding = typeof target.threadId === "number"
|
|
1300
1308
|
? deps.store.getWorkspaceBindingByTarget(target)
|
|
@@ -1304,7 +1312,7 @@ export function createTelegramSessionActionAssembly(deps) {
|
|
|
1304
1312
|
throw new Error("Telegram session replacement binding is unavailable.");
|
|
1305
1313
|
}
|
|
1306
1314
|
const createdAtMs = now();
|
|
1307
|
-
|
|
1315
|
+
const intent = {
|
|
1308
1316
|
continuity: binding ? "workspace-thread" : "classic-chat",
|
|
1309
1317
|
cwd: binding?.cwd ?? ctx.cwd,
|
|
1310
1318
|
profileName: deps.getProfileName() ?? "default",
|
|
@@ -1317,7 +1325,11 @@ export function createTelegramSessionActionAssembly(deps) {
|
|
|
1317
1325
|
? { threadName: binding.manualThreadName ?? binding.threadName } : {}),
|
|
1318
1326
|
createdAtMs,
|
|
1319
1327
|
expiresAtMs: createdAtMs + deps.handoffTtlMs,
|
|
1320
|
-
|
|
1328
|
+
...(follower ? { sourceInstanceId: follower.instanceId } : {}),
|
|
1329
|
+
};
|
|
1330
|
+
if (!await (follower
|
|
1331
|
+
? follower.requestSessionReplacement("publish", intent)
|
|
1332
|
+
: deps.store.commitSessionReplacementIntent(intent, deps.ownsPersistence))) {
|
|
1321
1333
|
throw new Error("Telegram session replacement intent was not persisted.");
|
|
1322
1334
|
}
|
|
1323
1335
|
},
|
|
@@ -1331,11 +1343,22 @@ export function createTelegramSessionActionAssembly(deps) {
|
|
|
1331
1343
|
return {
|
|
1332
1344
|
async getIntent() { await deps.store.refresh?.(); return deps.store.getSessionReplacementIntent(); },
|
|
1333
1345
|
hasSuccessorContinuity(intent) {
|
|
1334
|
-
|
|
1335
|
-
|
|
1346
|
+
if (intent.continuity === "classic-chat")
|
|
1347
|
+
return true;
|
|
1348
|
+
if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)?.cwd !==
|
|
1349
|
+
intent.cwd)
|
|
1350
|
+
return false;
|
|
1351
|
+
// A follower successor claims only after its own re-registration is live.
|
|
1352
|
+
return intent.sourceInstanceId === undefined || deps.ownsPersistence() ||
|
|
1353
|
+
deps.follower?.isRegisteredFor(intent.target) === true;
|
|
1336
1354
|
},
|
|
1337
1355
|
editSuccess(intent) { return deps.sendResult(intent.target, "<b>🆕 New session started.</b>"); },
|
|
1338
|
-
clearIntent(intent) {
|
|
1356
|
+
async clearIntent(intent) {
|
|
1357
|
+
if (intent.sourceInstanceId === undefined || deps.ownsPersistence()) {
|
|
1358
|
+
return deps.store.removeSessionReplacementIntent(intent, deps.ownsPersistence);
|
|
1359
|
+
}
|
|
1360
|
+
return await deps.follower?.requestSessionReplacement("settle", intent) ?? false;
|
|
1361
|
+
},
|
|
1339
1362
|
profileName: deps.getProfileName() ?? "default",
|
|
1340
1363
|
cwd: ctx.cwd,
|
|
1341
1364
|
sessionId,
|
|
@@ -60,6 +60,7 @@ const telegramBusProtocolIdentity = Bus.createTelegramCurrentBusProtocolIdentity
|
|
|
60
60
|
Bus.TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
61
61
|
Bus.TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
62
62
|
Bus.TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
63
|
+
Bus.TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
63
64
|
]);
|
|
64
65
|
// --- Extension Runtime ---
|
|
65
66
|
export default function (pi) {
|
|
@@ -191,6 +192,21 @@ export default function (pi) {
|
|
|
191
192
|
store: threadStore,
|
|
192
193
|
getProfileName: configStore.getActiveProfileName,
|
|
193
194
|
ownsPersistence: lockRuntime.owns,
|
|
195
|
+
follower: {
|
|
196
|
+
instanceId: telegramInstanceId,
|
|
197
|
+
isRegisteredFor(target) {
|
|
198
|
+
const registered = telegramBusFollowerRegistrationState.getTarget();
|
|
199
|
+
return telegramBusFollowerRegistrationState.isRegistered() &&
|
|
200
|
+
registered?.chatId === target.chatId &&
|
|
201
|
+
registered.threadId === target.threadId;
|
|
202
|
+
},
|
|
203
|
+
async requestSessionReplacement(operation, intent) {
|
|
204
|
+
const request = telegramBusFollowerRegistration.requestSessionReplacement;
|
|
205
|
+
if (!request)
|
|
206
|
+
return false;
|
|
207
|
+
return await request(operation, intent);
|
|
208
|
+
},
|
|
209
|
+
},
|
|
194
210
|
async sendResult(target, html) {
|
|
195
211
|
const delivery = await Delivery.sendTelegramView({ text: html, parseMode: "html", replyMarkup: { inline_keyboard: [] } }, { scope: { kind: "target", target } });
|
|
196
212
|
return delivery.ok ? { ok: true } : { ok: false,
|
|
@@ -1171,8 +1171,12 @@ export function createTelegramLockedPollingRuntime(deps) {
|
|
|
1171
1171
|
return;
|
|
1172
1172
|
if (canRestoreRememberedFollower && state?.kind === "active-elsewhere") {
|
|
1173
1173
|
const restored = await deps.restoreFollowerWithOwner?.(ctx, state.lock);
|
|
1174
|
-
if (!isCurrent()
|
|
1174
|
+
if (!isCurrent())
|
|
1175
1175
|
return;
|
|
1176
|
+
if (!restored) {
|
|
1177
|
+
deps.recordRuntimeEvent?.("lock", "Telegram follower auto-connect did not restore this session.", { phase: "follower-auto-connect-unavailable" });
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1176
1180
|
deps.onTransportAvailabilityChanged?.();
|
|
1177
1181
|
deps.updateStatus(ctx);
|
|
1178
1182
|
deps.recordRuntimeEvent?.("bus", "Telegram follower auto-connect completed", { phase: "follower-auto-connect" });
|
|
@@ -752,10 +752,14 @@ export function createTelegramThreadAwarePollingPorts(deps) {
|
|
|
752
752
|
return deps.registerFollowerWithLeader(ctx, owner);
|
|
753
753
|
};
|
|
754
754
|
const restoreFollowerWithOwner = async (ctx, owner) => {
|
|
755
|
-
if (!(await refreshFollowerState()))
|
|
755
|
+
if (!(await refreshFollowerState())) {
|
|
756
|
+
deps.recordEvent("bus", "Telegram follower auto-connect skipped: Threaded Mode is not enabled.", { phase: "follower-auto-connect-skip", reason: "thread-mode-unavailable" });
|
|
756
757
|
return undefined;
|
|
757
|
-
|
|
758
|
+
}
|
|
759
|
+
if (!deps.hasRememberedWorkspaceBinding?.(ctx)) {
|
|
760
|
+
deps.recordEvent("bus", "Telegram follower auto-connect skipped: no binding for this session.", { phase: "follower-auto-connect-skip", reason: "session-binding-unavailable" });
|
|
758
761
|
return undefined;
|
|
762
|
+
}
|
|
759
763
|
return deps.restoreFollowerWithLeader?.(ctx, owner);
|
|
760
764
|
};
|
|
761
765
|
return {
|
|
@@ -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;
|