@llblab/pi-kit 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +5 -5
- package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-actors/README.md +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
- package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
- package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
- package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
- package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
- package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
- package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
- package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
- package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
- package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
- package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
- package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
- package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
- package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
- package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
- package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
- package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
- package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
- package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
- package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
- package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
- package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
- package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
- package/node_modules/@llblab/pi-state-flow/README.md +203 -107
- package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
- package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
- package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
- package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
- package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
- package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
- package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
- package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
- package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
- package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
- package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
- package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
- package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
- package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
- package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
- package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
- package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
- package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
- package/node_modules/@llblab/pi-state-flow/package.json +8 -2
- package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
- package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -4
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +22 -5
- package/node_modules/@llblab/pi-telegram/README.md +13 -9
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +13 -7
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -5
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
- package/node_modules/@llblab/pi-telegram/index.ts +4 -1415
- package/node_modules/@llblab/pi-telegram/lib/activity.ts +19 -5
- package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +37 -2
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
- package/node_modules/@llblab/pi-telegram/lib/config.ts +178 -25
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -1
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +154 -15
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +74 -40
- package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +28 -42
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +18 -14
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +115 -70
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +66 -22
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +47 -39
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
- package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +315 -7
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
- package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
- package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
- package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
- package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
- package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
- package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
- package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
- package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
- package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
- package/node_modules/@llblab/skills/package.json +2 -3
- package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
- package/package.json +7 -6
|
@@ -10,7 +10,7 @@ import { basename } from "node:path";
|
|
|
10
10
|
|
|
11
11
|
import * as Sync from "./sync.ts";
|
|
12
12
|
import * as Threads from "./threads.ts";
|
|
13
|
-
import { parseTelegramUpdateJournalQueueOwner } from "./journal.ts";
|
|
13
|
+
import { parseTelegramUpdateJournalQueueOwner, type TelegramUpdateJournalStoreOptions } from "./journal.ts";
|
|
14
14
|
import type { TelegramLockEntry, TelegramLockState } from "./locks.ts";
|
|
15
15
|
import type {
|
|
16
16
|
TelegramQueueHandoffPayload,
|
|
@@ -31,19 +31,29 @@ import {
|
|
|
31
31
|
createUnauthorizedBusAck,
|
|
32
32
|
getTelegramBusProtocolCompatibility,
|
|
33
33
|
getTelegramBusSocketPath,
|
|
34
|
+
hasTelegramBusCapability,
|
|
34
35
|
isTelegramBusEnvelopeAuthorized,
|
|
35
36
|
resolveTelegramBusSocketPath,
|
|
36
37
|
sendTelegramBusLocalEnvelope,
|
|
37
38
|
type TelegramBusAgentMessage,
|
|
38
39
|
type TelegramBusAgentTargetSelector,
|
|
39
40
|
type TelegramBusEnvelope,
|
|
41
|
+
type TelegramBusForwardOwnership,
|
|
40
42
|
type TelegramBusProtocolIdentity,
|
|
41
43
|
type TelegramBusSocketPathSource,
|
|
44
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
45
|
+
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
42
46
|
} from "./bus.ts";
|
|
47
|
+
import type { TelegramConfigStore, TelegramThreadDisplayMode } from "./config.ts";
|
|
43
48
|
import {
|
|
44
49
|
getTelegramBusTransportRetryPolicy,
|
|
45
50
|
TELEGRAM_BUS_REGISTRATION_RETRY,
|
|
46
51
|
} from "./bus-transport.ts";
|
|
52
|
+
import {
|
|
53
|
+
createTelegramWorkspaceAdmissionOperationId,
|
|
54
|
+
runWithTelegramWorkspaceAdmissionsAsync,
|
|
55
|
+
type TelegramWorkspaceAdmissionLedger,
|
|
56
|
+
} from "./workspace-admission.ts";
|
|
47
57
|
|
|
48
58
|
export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
|
|
49
59
|
export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
|
|
@@ -109,10 +119,22 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
|
|
|
109
119
|
registerWithLeader: (
|
|
110
120
|
ctx: TContext,
|
|
111
121
|
leader: { busSocketPath?: string; busSecret?: string },
|
|
112
|
-
options?: {
|
|
122
|
+
options?: {
|
|
123
|
+
target?: TelegramTarget;
|
|
124
|
+
previousInstanceId?: string;
|
|
125
|
+
restoreWorkspace?: boolean;
|
|
126
|
+
},
|
|
113
127
|
) => Promise<boolean>;
|
|
114
128
|
setContext: (ctx: TContext) => void;
|
|
115
129
|
disconnectFromLeader?: () => Promise<boolean>;
|
|
130
|
+
renameThread?: (
|
|
131
|
+
target: TelegramTarget & { threadId: number },
|
|
132
|
+
threadName: string,
|
|
133
|
+
) => Promise<string>;
|
|
134
|
+
resetThreadName?: (
|
|
135
|
+
target: TelegramTarget & { threadId: number },
|
|
136
|
+
) => Promise<string>;
|
|
137
|
+
setThreadDisplayMode?: (mode: TelegramThreadDisplayMode) => Promise<void>;
|
|
116
138
|
stop: () => void;
|
|
117
139
|
}
|
|
118
140
|
|
|
@@ -168,6 +190,8 @@ export interface TelegramBusFollowerRegistrationState {
|
|
|
168
190
|
getTarget: () => TelegramTarget | undefined;
|
|
169
191
|
getSlot: () => string | undefined;
|
|
170
192
|
getThreadName: () => string | undefined;
|
|
193
|
+
getDisplayTitle: () => string | undefined;
|
|
194
|
+
setDisplayTitle: (title: string, generation: string) => boolean;
|
|
171
195
|
getGeneration: () => string | undefined;
|
|
172
196
|
beginRecovery: () => number;
|
|
173
197
|
cancelRecovery: () => void;
|
|
@@ -181,6 +205,7 @@ export interface TelegramBusFollowerRegistrationState {
|
|
|
181
205
|
metadata?: {
|
|
182
206
|
slot?: string;
|
|
183
207
|
threadName?: string;
|
|
208
|
+
displayTitle?: string;
|
|
184
209
|
generation?: string;
|
|
185
210
|
leaderProtocol?: TelegramBusProtocolIdentity;
|
|
186
211
|
},
|
|
@@ -206,7 +231,8 @@ export interface TelegramBusFollowerDurableAdmissionPort<TContext> {
|
|
|
206
231
|
| "leader.forwardCallback"
|
|
207
232
|
| "leader.forwardReaction"
|
|
208
233
|
| "leader.forwardMessage"
|
|
209
|
-
| "leader.forwardEditedMessage"
|
|
234
|
+
| "leader.forwardEditedMessage"
|
|
235
|
+
| "leader.wakeInputCustody";
|
|
210
236
|
}
|
|
211
237
|
>,
|
|
212
238
|
ctx: TContext,
|
|
@@ -225,6 +251,7 @@ export interface TelegramBusFollowerClientRuntimeDeps<TMessage = unknown> {
|
|
|
225
251
|
getForwardCommentBatchPosition?: (
|
|
226
252
|
message: TMessage,
|
|
227
253
|
) => "comment" | "forward" | undefined;
|
|
254
|
+
validateForwardOwnership?: (ownership: TelegramBusForwardOwnership) => boolean;
|
|
228
255
|
recordRuntimeEvent?: (
|
|
229
256
|
category: string,
|
|
230
257
|
error: unknown,
|
|
@@ -279,6 +306,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
|
|
|
279
306
|
) => void;
|
|
280
307
|
onHeartbeatFailure?: (error: unknown, ctx: TContext) => Promise<void> | void;
|
|
281
308
|
onRegistered?: (ctx: TContext) => Promise<void> | void;
|
|
309
|
+
onDisplayTitleChanged?: (ctx: TContext) => void;
|
|
282
310
|
}
|
|
283
311
|
|
|
284
312
|
export function createTelegramManualFollowerProfileKeyResolver(input: {
|
|
@@ -303,6 +331,43 @@ export type TelegramBusFollowerPromotionHandler<TContext> = (
|
|
|
303
331
|
election: TelegramBusFollowerElection,
|
|
304
332
|
) => Promise<boolean>;
|
|
305
333
|
|
|
334
|
+
type TelegramBusFollowerWorkspaceAdmissionDeps = {
|
|
335
|
+
getWorkspaceAdmission?: () => Pick<
|
|
336
|
+
TelegramWorkspaceAdmissionLedger,
|
|
337
|
+
"acquireAdmission" | "releaseAdmission"
|
|
338
|
+
> | undefined;
|
|
339
|
+
recordRuntimeEvent?: (
|
|
340
|
+
category: string,
|
|
341
|
+
error: unknown,
|
|
342
|
+
details?: Record<string, unknown>,
|
|
343
|
+
) => void;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
function runTelegramBusFollowerWorkspaceMutation<T>(
|
|
347
|
+
deps: TelegramBusFollowerWorkspaceAdmissionDeps,
|
|
348
|
+
operationKind: string,
|
|
349
|
+
operation: () => Promise<T>,
|
|
350
|
+
): Promise<T> {
|
|
351
|
+
if (!deps.getWorkspaceAdmission) return operation();
|
|
352
|
+
const admission = deps.getWorkspaceAdmission();
|
|
353
|
+
if (!admission) {
|
|
354
|
+
throw new Error("Telegram Workspace admission authority is unavailable.");
|
|
355
|
+
}
|
|
356
|
+
return runWithTelegramWorkspaceAdmissionsAsync({
|
|
357
|
+
ledger: admission,
|
|
358
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
359
|
+
operationKind,
|
|
360
|
+
scopes: [{ kind: "profile" }],
|
|
361
|
+
operation,
|
|
362
|
+
onReleaseError(error) {
|
|
363
|
+
deps.recordRuntimeEvent?.("bus", error, {
|
|
364
|
+
phase: "workspace-admission-release",
|
|
365
|
+
operationKind,
|
|
366
|
+
});
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
306
371
|
export function createTelegramBusFollowerPromotionHandler<
|
|
307
372
|
TContext extends { cwd: string },
|
|
308
373
|
>(input: {
|
|
@@ -321,66 +386,82 @@ export function createTelegramBusFollowerPromotionHandler<
|
|
|
321
386
|
) => void;
|
|
322
387
|
getNowMs?: () => number;
|
|
323
388
|
getPid?: () => number;
|
|
389
|
+
getWorkspaceAdmission?:
|
|
390
|
+
TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"];
|
|
324
391
|
}): TelegramBusFollowerPromotionHandler<TContext> {
|
|
325
|
-
return
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
392
|
+
return (ctx, binding, election) => runTelegramBusFollowerWorkspaceMutation(
|
|
393
|
+
input,
|
|
394
|
+
"workspace.promote-follower",
|
|
395
|
+
async () => {
|
|
396
|
+
let promotedRecord: Threads.TelegramTopicTargetRecord | undefined;
|
|
397
|
+
const promoted = await input.startLeader(ctx, election, async () => {
|
|
398
|
+
promotedRecord =
|
|
399
|
+
await Threads.promoteTelegramFollowerBindingToLeader({
|
|
400
|
+
store: input.topicTargetStore,
|
|
401
|
+
instanceId: input.instanceId,
|
|
402
|
+
cwd: ctx.cwd,
|
|
403
|
+
telegramProfile: input.getActiveProfileName(),
|
|
404
|
+
target: binding.target,
|
|
405
|
+
slot: binding.slot,
|
|
406
|
+
threadName: binding.threadName,
|
|
407
|
+
});
|
|
408
|
+
if (!promotedRecord && typeof binding.target?.threadId === "number") {
|
|
409
|
+
throw new Error(
|
|
410
|
+
"Telegram follower promotion slot authority is unavailable.",
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
if (promotedRecord) {
|
|
414
|
+
input.recordRuntimeEvent(
|
|
415
|
+
"bus",
|
|
416
|
+
"Follower thread binding promoted to leader",
|
|
417
|
+
{
|
|
418
|
+
phase: "follower-promoted-binding",
|
|
419
|
+
chatId: promotedRecord.target.chatId,
|
|
420
|
+
threadId: promotedRecord.target.threadId,
|
|
421
|
+
slot: promotedRecord.slot,
|
|
422
|
+
threadName: promotedRecord.threadName,
|
|
423
|
+
},
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
if (
|
|
428
|
+
promoted &&
|
|
429
|
+
promotedRecord &&
|
|
430
|
+
typeof binding.target?.threadId === "number"
|
|
431
|
+
) {
|
|
432
|
+
const profileKey = Threads.getTelegramThreadOwnerKey({
|
|
433
|
+
kind: "leader",
|
|
331
434
|
cwd: ctx.cwd,
|
|
435
|
+
instanceId: input.instanceId,
|
|
332
436
|
telegramProfile: input.getActiveProfileName(),
|
|
333
|
-
target: binding.target,
|
|
334
|
-
slot: binding.slot,
|
|
335
|
-
threadName: binding.threadName,
|
|
336
437
|
});
|
|
337
|
-
|
|
438
|
+
Threads.setTelegramLeaderSessionHandoff({
|
|
439
|
+
pid: input.getPid?.() ?? process.pid,
|
|
440
|
+
instanceId: input.instanceId,
|
|
441
|
+
createdAtMs: input.getNowMs?.() ?? Date.now(),
|
|
442
|
+
profileKey,
|
|
443
|
+
target: {
|
|
444
|
+
chatId: binding.target.chatId,
|
|
445
|
+
threadId: binding.target.threadId,
|
|
446
|
+
},
|
|
447
|
+
slot: promotedRecord.slot,
|
|
448
|
+
threadName: promotedRecord.threadName,
|
|
449
|
+
});
|
|
338
450
|
input.recordRuntimeEvent(
|
|
339
451
|
"bus",
|
|
340
|
-
"
|
|
452
|
+
"Promoted leader binding retained for session replacement",
|
|
341
453
|
{
|
|
342
|
-
phase: "follower-promoted-
|
|
343
|
-
chatId:
|
|
344
|
-
threadId:
|
|
454
|
+
phase: "follower-promoted-session-handoff",
|
|
455
|
+
chatId: binding.target.chatId,
|
|
456
|
+
threadId: binding.target.threadId,
|
|
345
457
|
slot: promotedRecord.slot,
|
|
346
458
|
threadName: promotedRecord.threadName,
|
|
347
459
|
},
|
|
348
460
|
);
|
|
349
461
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
kind: "leader",
|
|
354
|
-
cwd: ctx.cwd,
|
|
355
|
-
instanceId: input.instanceId,
|
|
356
|
-
telegramProfile: input.getActiveProfileName(),
|
|
357
|
-
});
|
|
358
|
-
Threads.setTelegramLeaderSessionHandoff({
|
|
359
|
-
pid: input.getPid?.() ?? process.pid,
|
|
360
|
-
instanceId: input.instanceId,
|
|
361
|
-
createdAtMs: input.getNowMs?.() ?? Date.now(),
|
|
362
|
-
profileKey,
|
|
363
|
-
target: {
|
|
364
|
-
chatId: binding.target.chatId,
|
|
365
|
-
threadId: binding.target.threadId,
|
|
366
|
-
},
|
|
367
|
-
slot: binding.slot,
|
|
368
|
-
threadName: binding.threadName,
|
|
369
|
-
});
|
|
370
|
-
input.recordRuntimeEvent(
|
|
371
|
-
"bus",
|
|
372
|
-
"Promoted leader binding retained for session replacement",
|
|
373
|
-
{
|
|
374
|
-
phase: "follower-promoted-session-handoff",
|
|
375
|
-
chatId: binding.target.chatId,
|
|
376
|
-
threadId: binding.target.threadId,
|
|
377
|
-
slot: binding.slot,
|
|
378
|
-
threadName: binding.threadName,
|
|
379
|
-
},
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
return promoted;
|
|
383
|
-
};
|
|
462
|
+
return promoted;
|
|
463
|
+
},
|
|
464
|
+
);
|
|
384
465
|
}
|
|
385
466
|
|
|
386
467
|
export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
|
|
@@ -390,7 +471,7 @@ export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
|
|
|
390
471
|
>;
|
|
391
472
|
registrationState: Pick<
|
|
392
473
|
TelegramBusFollowerRegistrationState,
|
|
393
|
-
"getTarget" | "setRegistered" | "getGeneration"
|
|
474
|
+
"getTarget" | "getSlot" | "setRegistered" | "getGeneration"
|
|
394
475
|
>;
|
|
395
476
|
instanceId: string;
|
|
396
477
|
getManualFollowerProfileKey: () => string;
|
|
@@ -404,6 +485,7 @@ export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
|
|
|
404
485
|
error: unknown,
|
|
405
486
|
details?: Record<string, unknown>,
|
|
406
487
|
) => void;
|
|
488
|
+
getWorkspaceAdmission?: TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"];
|
|
407
489
|
}
|
|
408
490
|
|
|
409
491
|
export type TelegramBusFollowerLeaderState =
|
|
@@ -457,7 +539,14 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
|
|
|
457
539
|
getRegistrationGeneration: () => string | undefined;
|
|
458
540
|
getRecipientBindingKey: () => string | undefined;
|
|
459
541
|
durableAdmission: TelegramBusFollowerDurableAdmissionPort<TContext>;
|
|
542
|
+
sourceReferenceAdmission?: TelegramBusFollowerDurableAdmissionPort<TContext>;
|
|
543
|
+
isSourceReferenceAdmissionEnabled?: () => boolean;
|
|
544
|
+
hasAuthenticatedSourceReferenceTransport?: () => boolean;
|
|
460
545
|
getContext: () => TContext | undefined;
|
|
546
|
+
handleInputCustodyHandoff?: (
|
|
547
|
+
envelope: Extract<TelegramBusEnvelope, { kind: "leader.offerInputCustodyHandoff" }>,
|
|
548
|
+
ctx: TContext,
|
|
549
|
+
) => Promise<unknown> | unknown;
|
|
461
550
|
handleQueueHandoff?: (
|
|
462
551
|
envelope: Extract<
|
|
463
552
|
TelegramBusEnvelope,
|
|
@@ -568,83 +657,106 @@ export function createTelegramBusFollowerTargetReplacementHandler<TContext>(
|
|
|
568
657
|
TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>["handleReplaceTarget"]
|
|
569
658
|
> {
|
|
570
659
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
571
|
-
return
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
660
|
+
return (input, ctx) => runTelegramBusFollowerWorkspaceMutation(
|
|
661
|
+
deps,
|
|
662
|
+
"workspace.replace-follower-target",
|
|
663
|
+
async () => {
|
|
664
|
+
const assertCurrent = (expectedTarget = input.oldTarget): void => {
|
|
665
|
+
const target = deps.registrationState.getTarget();
|
|
666
|
+
if (
|
|
667
|
+
!input.registrationGeneration ||
|
|
668
|
+
deps.registrationState.getGeneration() !==
|
|
669
|
+
input.registrationGeneration ||
|
|
670
|
+
!input.oldTarget ||
|
|
671
|
+
!expectedTarget ||
|
|
672
|
+
target?.chatId !== expectedTarget.chatId ||
|
|
673
|
+
target.threadId !== expectedTarget.threadId ||
|
|
674
|
+
input.target.chatId !== input.oldTarget.chatId ||
|
|
675
|
+
input.target.threadId === input.oldTarget.threadId
|
|
676
|
+
) {
|
|
677
|
+
throw new Error(
|
|
678
|
+
"Stale Telegram follower target replacement authority.",
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
assertCurrent();
|
|
683
|
+
await deps.topicTargetStore.load();
|
|
684
|
+
assertCurrent();
|
|
685
|
+
const nowMs = getNowMs();
|
|
686
|
+
const currentRecord = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
687
|
+
records: deps.topicTargetStore.list(),
|
|
688
|
+
instanceId: deps.instanceId,
|
|
689
|
+
preferredTarget: input.oldTarget,
|
|
690
|
+
});
|
|
691
|
+
if (
|
|
692
|
+
currentRecord &&
|
|
693
|
+
(currentRecord.target.chatId !== input.oldTarget!.chatId ||
|
|
694
|
+
currentRecord.target.threadId !== input.oldTarget!.threadId)
|
|
695
|
+
) {
|
|
696
|
+
throw new Error("Stale Telegram follower binding replacement target.");
|
|
581
697
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
instanceId: deps.
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
slot: currentRecord?.slot,
|
|
645
|
-
},
|
|
646
|
-
);
|
|
647
|
-
};
|
|
698
|
+
const currentSlot =
|
|
699
|
+
currentRecord?.slot ?? deps.registrationState.getSlot();
|
|
700
|
+
if (!currentSlot || !/^[A-Z]$/u.test(currentSlot)) {
|
|
701
|
+
throw new Error("Telegram Thread slot authority is unavailable.");
|
|
702
|
+
}
|
|
703
|
+
deps.topicTargetStore.markStaleByTarget(
|
|
704
|
+
input.oldTarget!,
|
|
705
|
+
"deleted",
|
|
706
|
+
"Follower thread was replaced by thread restore.",
|
|
707
|
+
);
|
|
708
|
+
const profileKey =
|
|
709
|
+
currentRecord?.profileKey ?? deps.getManualFollowerProfileKey();
|
|
710
|
+
deps.topicTargetStore.upsert({
|
|
711
|
+
profileKey,
|
|
712
|
+
owner: {
|
|
713
|
+
kind: "manual-follower",
|
|
714
|
+
instanceId: deps.manualFollowerOwnerId,
|
|
715
|
+
},
|
|
716
|
+
target: input.target,
|
|
717
|
+
status: "active",
|
|
718
|
+
syncStatus: "open",
|
|
719
|
+
createdAtMs: currentRecord?.createdAtMs ?? nowMs,
|
|
720
|
+
updatedAtMs: nowMs,
|
|
721
|
+
lastSyncObservedAtMs: nowMs,
|
|
722
|
+
lastReconcileAction: "follower-thread-restore",
|
|
723
|
+
instanceId: deps.instanceId,
|
|
724
|
+
slot: currentSlot,
|
|
725
|
+
threadName: currentRecord?.threadName,
|
|
726
|
+
rerouteConfirmedAtMs: nowMs,
|
|
727
|
+
});
|
|
728
|
+
deps.registrationState.setRegistered(true, input.target, {
|
|
729
|
+
slot: currentSlot,
|
|
730
|
+
threadName: currentRecord?.threadName,
|
|
731
|
+
generation: input.registrationGeneration,
|
|
732
|
+
});
|
|
733
|
+
await deps.topicTargetStore.persist();
|
|
734
|
+
assertCurrent(input.target);
|
|
735
|
+
deps.setSyncState(
|
|
736
|
+
Sync.markTelegramSyncSliceFresh(
|
|
737
|
+
deps.getSyncState(),
|
|
738
|
+
"target-bindings",
|
|
739
|
+
{
|
|
740
|
+
nowMs,
|
|
741
|
+
action: "follower-thread-restore",
|
|
742
|
+
},
|
|
743
|
+
),
|
|
744
|
+
);
|
|
745
|
+
deps.updateStatus(ctx);
|
|
746
|
+
deps.recordRuntimeEvent?.(
|
|
747
|
+
"bus",
|
|
748
|
+
"Telegram follower thread target replaced",
|
|
749
|
+
{
|
|
750
|
+
phase: "follower-thread-restore",
|
|
751
|
+
chatId: input.target.chatId,
|
|
752
|
+
threadId: input.target.threadId,
|
|
753
|
+
oldThreadId:
|
|
754
|
+
input.oldTarget?.threadId ?? currentRecord?.target.threadId,
|
|
755
|
+
slot: currentSlot,
|
|
756
|
+
},
|
|
757
|
+
);
|
|
758
|
+
},
|
|
759
|
+
);
|
|
648
760
|
}
|
|
649
761
|
|
|
650
762
|
export function createTelegramBusFollowerClientRuntime<
|
|
@@ -686,6 +798,7 @@ export function createTelegramBusFollowerClientRuntime<
|
|
|
686
798
|
getAuthSecret: deps.getForwardingAuthSecret,
|
|
687
799
|
getForwardCommentBatchPosition:
|
|
688
800
|
deps.getForwardCommentBatchPosition,
|
|
801
|
+
validateForwardOwnership: deps.validateForwardOwnership,
|
|
689
802
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
690
803
|
}),
|
|
691
804
|
queueHandoff: createTelegramBusFollowerQueueHandoffClient({
|
|
@@ -1109,6 +1222,7 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1109
1222
|
let target: TelegramTarget | undefined;
|
|
1110
1223
|
let slot: string | undefined;
|
|
1111
1224
|
let threadName: string | undefined;
|
|
1225
|
+
let displayTitle: string | undefined;
|
|
1112
1226
|
let generation: string | undefined;
|
|
1113
1227
|
let leaderProtocol: TelegramBusProtocolIdentity | undefined;
|
|
1114
1228
|
let eligibleElectionSlots: string[] = [];
|
|
@@ -1131,6 +1245,13 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1131
1245
|
getTarget: () => (target ? { ...target } : undefined),
|
|
1132
1246
|
getSlot: () => slot,
|
|
1133
1247
|
getThreadName: () => threadName,
|
|
1248
|
+
getDisplayTitle: () => displayTitle,
|
|
1249
|
+
setDisplayTitle(title, expectedGeneration) {
|
|
1250
|
+
if (!registered || !generation || expectedGeneration !== generation ||
|
|
1251
|
+
!title.trim() || title.length > 128 || displayTitle === title) return false;
|
|
1252
|
+
displayTitle = title;
|
|
1253
|
+
return true;
|
|
1254
|
+
},
|
|
1134
1255
|
getGeneration: () => generation,
|
|
1135
1256
|
beginRecovery: () => {
|
|
1136
1257
|
if (activeRecoveryEpoch !== undefined) return activeRecoveryEpoch;
|
|
@@ -1169,11 +1290,18 @@ export function createTelegramBusFollowerRegistrationState(
|
|
|
1169
1290
|
).sort();
|
|
1170
1291
|
},
|
|
1171
1292
|
setRegistered: (next, nextTarget, metadata) => {
|
|
1293
|
+
const retainedDisplayTitle = next && registered &&
|
|
1294
|
+
generation === metadata?.generation &&
|
|
1295
|
+
target?.chatId === nextTarget?.chatId && target?.threadId === nextTarget?.threadId
|
|
1296
|
+
? displayTitle : undefined;
|
|
1172
1297
|
const availabilityChanged = registered !== next;
|
|
1173
1298
|
registered = next;
|
|
1174
1299
|
target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
|
|
1175
1300
|
slot = next ? metadata?.slot : undefined;
|
|
1176
1301
|
threadName = next ? metadata?.threadName : undefined;
|
|
1302
|
+
displayTitle = next && nextTarget && metadata?.generation &&
|
|
1303
|
+
metadata.displayTitle?.trim() && metadata.displayTitle.length <= 128
|
|
1304
|
+
? metadata.displayTitle : retainedDisplayTitle;
|
|
1177
1305
|
generation = next ? metadata?.generation : undefined;
|
|
1178
1306
|
leaderProtocol =
|
|
1179
1307
|
next && metadata?.leaderProtocol
|
|
@@ -1530,6 +1658,15 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1530
1658
|
)
|
|
1531
1659
|
: [];
|
|
1532
1660
|
deps.registrationState?.setEligibleElectionSlots(slots);
|
|
1661
|
+
if (typeof heartbeatResult?.displayTitle === "string" &&
|
|
1662
|
+
deps.registrationState?.setDisplayTitle(heartbeatResult.displayTitle, registrationGeneration) &&
|
|
1663
|
+
heartbeatContext) {
|
|
1664
|
+
try {
|
|
1665
|
+
deps.onDisplayTitleChanged?.(heartbeatContext);
|
|
1666
|
+
} catch {
|
|
1667
|
+
// Display refresh failure must not invalidate a successful heartbeat.
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1533
1670
|
}
|
|
1534
1671
|
if (response?.kind === "bus.ack" && !response.ok) {
|
|
1535
1672
|
throw new Error(
|
|
@@ -1596,7 +1733,11 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1596
1733
|
previousInstanceId: pendingHandoff.instanceId,
|
|
1597
1734
|
}
|
|
1598
1735
|
: undefined;
|
|
1599
|
-
const registrationOptions
|
|
1736
|
+
const registrationOptions: {
|
|
1737
|
+
target?: TelegramTarget;
|
|
1738
|
+
previousInstanceId?: string;
|
|
1739
|
+
restoreWorkspace?: boolean;
|
|
1740
|
+
} | undefined = options ?? pendingHandoffOptions;
|
|
1600
1741
|
const leaderSocketPath =
|
|
1601
1742
|
leader.busSocketPath ??
|
|
1602
1743
|
deps.getLeaderSocketPath?.() ??
|
|
@@ -1607,11 +1748,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1607
1748
|
: leader?.busSecret;
|
|
1608
1749
|
deps.setActiveAuthSecret?.(activeAuthSecret);
|
|
1609
1750
|
const registrationGeneration = deps.createRequestId();
|
|
1610
|
-
const registrationEnvelope:
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
kind: "follower.register",
|
|
1751
|
+
const registrationEnvelope: TelegramBusEnvelope = {
|
|
1752
|
+
kind: registrationOptions?.restoreWorkspace
|
|
1753
|
+
? "follower.restoreWorkspace"
|
|
1754
|
+
: "follower.register",
|
|
1615
1755
|
requestId: registrationGeneration,
|
|
1616
1756
|
auth: activeAuthSecret,
|
|
1617
1757
|
registration: {
|
|
@@ -1705,6 +1845,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1705
1845
|
deps.registrationState?.setRegistered(false);
|
|
1706
1846
|
deps.setActiveAuthSecret?.(undefined);
|
|
1707
1847
|
await deps.stopReceiving?.();
|
|
1848
|
+
if (
|
|
1849
|
+
registrationOptions?.restoreWorkspace &&
|
|
1850
|
+
response.error?.code === "workspace-binding-unavailable"
|
|
1851
|
+
) return false;
|
|
1708
1852
|
throw new Error(
|
|
1709
1853
|
response.message ??
|
|
1710
1854
|
"Telegram bus follower registration was rejected.",
|
|
@@ -1759,6 +1903,168 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1759
1903
|
setContext(ctx) {
|
|
1760
1904
|
activeContext = ctx;
|
|
1761
1905
|
},
|
|
1906
|
+
async setThreadDisplayMode(mode) {
|
|
1907
|
+
const socketPath = activeLeaderSocketPath;
|
|
1908
|
+
const generation = activeRegistrationGeneration;
|
|
1909
|
+
const auth = activeAuthSecret;
|
|
1910
|
+
if (!socketPath || !generation || !deps.registrationState?.isRegistered()) {
|
|
1911
|
+
throw new Error("Telegram follower is not registered with the leader.");
|
|
1912
|
+
}
|
|
1913
|
+
if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) ||
|
|
1914
|
+
!hasTelegramBusCapability(deps.registrationState.getLeaderProtocol(), TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE)) {
|
|
1915
|
+
throw new Error("The Telegram peers do not support Thread display settings. Update or restart both instances.");
|
|
1916
|
+
}
|
|
1917
|
+
const requestId = deps.createRequestId();
|
|
1918
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
1919
|
+
socketPath, timeoutMs: registrationTimeoutMs,
|
|
1920
|
+
envelope: {
|
|
1921
|
+
kind: "follower.setThreadDisplayMode", requestId, auth,
|
|
1922
|
+
instanceId: deps.instanceId, registrationGeneration: generation, mode,
|
|
1923
|
+
},
|
|
1924
|
+
});
|
|
1925
|
+
if (activeLeaderSocketPath !== socketPath || activeRegistrationGeneration !== generation ||
|
|
1926
|
+
activeAuthSecret !== auth || deps.registrationState.getGeneration() !== generation) {
|
|
1927
|
+
throw new Error("Telegram Thread display setting completed for a stale registration.");
|
|
1928
|
+
}
|
|
1929
|
+
if (response?.kind !== "bus.ack" || !response.ok || response.requestId !== requestId ||
|
|
1930
|
+
!isRecord(response.result) || response.result.mode !== mode) {
|
|
1931
|
+
throw new Error(response?.kind === "bus.ack"
|
|
1932
|
+
? response.message ?? "Telegram Thread display setting was rejected."
|
|
1933
|
+
: "Telegram Thread display setting was not acknowledged.");
|
|
1934
|
+
}
|
|
1935
|
+
},
|
|
1936
|
+
async renameThread(target, threadName) {
|
|
1937
|
+
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
1938
|
+
throw new Error("Telegram follower is not registered with the leader.");
|
|
1939
|
+
}
|
|
1940
|
+
if (
|
|
1941
|
+
deps.registrationState &&
|
|
1942
|
+
!hasTelegramBusCapability(
|
|
1943
|
+
deps.registrationState.getLeaderProtocol(),
|
|
1944
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
1945
|
+
)
|
|
1946
|
+
) {
|
|
1947
|
+
throw new Error(
|
|
1948
|
+
"The active Telegram leader does not support Workspace Thread rename. Update or restart that Pi instance.",
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
const expectedLeaderSocketPath = activeLeaderSocketPath;
|
|
1952
|
+
const expectedRegistrationGeneration = activeRegistrationGeneration;
|
|
1953
|
+
const expectedAuthSecret = activeAuthSecret;
|
|
1954
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
1955
|
+
socketPath: expectedLeaderSocketPath,
|
|
1956
|
+
timeoutMs: registrationTimeoutMs,
|
|
1957
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
1958
|
+
endpoint: expectedLeaderSocketPath,
|
|
1959
|
+
operation: "operation",
|
|
1960
|
+
}),
|
|
1961
|
+
envelope: {
|
|
1962
|
+
kind: "follower.renameThread",
|
|
1963
|
+
requestId: deps.createRequestId(),
|
|
1964
|
+
auth: expectedAuthSecret,
|
|
1965
|
+
instanceId: deps.instanceId,
|
|
1966
|
+
registrationGeneration: expectedRegistrationGeneration,
|
|
1967
|
+
target,
|
|
1968
|
+
threadName,
|
|
1969
|
+
sentAtMs: getNowMs(),
|
|
1970
|
+
},
|
|
1971
|
+
});
|
|
1972
|
+
if (response?.kind !== "bus.ack" || !response.ok) {
|
|
1973
|
+
throw new Error(
|
|
1974
|
+
response?.kind === "bus.ack"
|
|
1975
|
+
? (response.message ?? "Telegram Workspace Thread rename was rejected.")
|
|
1976
|
+
: "Telegram Workspace Thread rename was not acknowledged.",
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
if (
|
|
1980
|
+
activeLeaderSocketPath !== expectedLeaderSocketPath ||
|
|
1981
|
+
activeRegistrationGeneration !== expectedRegistrationGeneration ||
|
|
1982
|
+
(deps.registrationState &&
|
|
1983
|
+
deps.registrationState.getGeneration() !==
|
|
1984
|
+
expectedRegistrationGeneration)
|
|
1985
|
+
) {
|
|
1986
|
+
throw new Error(
|
|
1987
|
+
"Telegram Workspace Thread rename completed for a stale follower registration.",
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
const renamedThreadName =
|
|
1991
|
+
response.result &&
|
|
1992
|
+
typeof response.result === "object" &&
|
|
1993
|
+
"threadName" in response.result &&
|
|
1994
|
+
typeof response.result.threadName === "string"
|
|
1995
|
+
? response.result.threadName
|
|
1996
|
+
: threadName;
|
|
1997
|
+
const registrationTarget = deps.registrationState?.getTarget();
|
|
1998
|
+
if (deps.registrationState && registrationTarget) {
|
|
1999
|
+
deps.registrationState.setRegistered(true, registrationTarget, {
|
|
2000
|
+
slot: deps.registrationState.getSlot(),
|
|
2001
|
+
threadName: renamedThreadName,
|
|
2002
|
+
generation: expectedRegistrationGeneration,
|
|
2003
|
+
leaderProtocol: deps.registrationState.getLeaderProtocol(),
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
return renamedThreadName;
|
|
2007
|
+
},
|
|
2008
|
+
async resetThreadName(target) {
|
|
2009
|
+
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
2010
|
+
throw new Error("Telegram follower is not registered with the leader.");
|
|
2011
|
+
}
|
|
2012
|
+
if (deps.registrationState && !hasTelegramBusCapability(
|
|
2013
|
+
deps.registrationState.getLeaderProtocol(),
|
|
2014
|
+
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
2015
|
+
)) {
|
|
2016
|
+
throw new Error(
|
|
2017
|
+
"The active Telegram leader does not support Workspace Thread reset. Update or restart that Pi instance.",
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
const expectedLeaderSocketPath = activeLeaderSocketPath;
|
|
2021
|
+
const expectedRegistrationGeneration = activeRegistrationGeneration;
|
|
2022
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
2023
|
+
socketPath: expectedLeaderSocketPath,
|
|
2024
|
+
timeoutMs: registrationTimeoutMs,
|
|
2025
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
2026
|
+
endpoint: expectedLeaderSocketPath,
|
|
2027
|
+
operation: "operation",
|
|
2028
|
+
}),
|
|
2029
|
+
envelope: {
|
|
2030
|
+
kind: "follower.resetThreadName",
|
|
2031
|
+
requestId: deps.createRequestId(),
|
|
2032
|
+
auth: activeAuthSecret,
|
|
2033
|
+
instanceId: deps.instanceId,
|
|
2034
|
+
registrationGeneration: expectedRegistrationGeneration,
|
|
2035
|
+
target,
|
|
2036
|
+
sentAtMs: getNowMs(),
|
|
2037
|
+
},
|
|
2038
|
+
});
|
|
2039
|
+
const resetName = response?.kind === "bus.ack" && response.ok &&
|
|
2040
|
+
response.result && typeof response.result === "object" &&
|
|
2041
|
+
"threadName" in response.result &&
|
|
2042
|
+
typeof response.result.threadName === "string"
|
|
2043
|
+
? response.result.threadName : undefined;
|
|
2044
|
+
if (!resetName) {
|
|
2045
|
+
throw new Error(response?.kind === "bus.ack"
|
|
2046
|
+
? response.message ?? "Telegram Workspace Thread reset was rejected."
|
|
2047
|
+
: "Telegram Workspace Thread reset was not acknowledged.");
|
|
2048
|
+
}
|
|
2049
|
+
if (activeLeaderSocketPath !== expectedLeaderSocketPath ||
|
|
2050
|
+
activeRegistrationGeneration !== expectedRegistrationGeneration ||
|
|
2051
|
+
(deps.registrationState && deps.registrationState.getGeneration() !==
|
|
2052
|
+
expectedRegistrationGeneration)) {
|
|
2053
|
+
throw new Error(
|
|
2054
|
+
"Telegram Workspace Thread reset completed for a stale follower registration.",
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
const registrationTarget = deps.registrationState?.getTarget();
|
|
2058
|
+
if (deps.registrationState && registrationTarget) {
|
|
2059
|
+
deps.registrationState.setRegistered(true, registrationTarget, {
|
|
2060
|
+
slot: deps.registrationState.getSlot(),
|
|
2061
|
+
threadName: resetName,
|
|
2062
|
+
generation: expectedRegistrationGeneration,
|
|
2063
|
+
leaderProtocol: deps.registrationState.getLeaderProtocol(),
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
return resetName;
|
|
2067
|
+
},
|
|
1762
2068
|
async disconnectFromLeader() {
|
|
1763
2069
|
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
1764
2070
|
return false;
|
|
@@ -1793,6 +2099,38 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1793
2099
|
const TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD =
|
|
1794
2100
|
"pi_telegram_forward_comment_batch_position";
|
|
1795
2101
|
|
|
2102
|
+
/** Bind source-owned sender checks to the journal's already-admitted synchronous v1 hook. */
|
|
2103
|
+
export function createTelegramBusFollowerPairedAdmission(deps: {
|
|
2104
|
+
profileName: string;
|
|
2105
|
+
tokenSha256: string;
|
|
2106
|
+
configStore: Pick<TelegramConfigStore, "withPairedUserAdmission">;
|
|
2107
|
+
assertExecutionCurrent: () => void;
|
|
2108
|
+
}): NonNullable<TelegramUpdateJournalStoreOptions["withPairedAdmission"]> {
|
|
2109
|
+
return (updates, publish) => {
|
|
2110
|
+
if (updates.length !== 1) return { admitted: false };
|
|
2111
|
+
const update = updates[0]!;
|
|
2112
|
+
const keys = Object.keys(update).filter((key) =>
|
|
2113
|
+
key !== "update_id" && key !== TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD);
|
|
2114
|
+
if (keys.length !== 1) return { admitted: false };
|
|
2115
|
+
const kind = keys[0];
|
|
2116
|
+
if (kind !== "message" && kind !== "edited_message" &&
|
|
2117
|
+
kind !== "callback_query" && kind !== "message_reaction") return { admitted: false };
|
|
2118
|
+
const position = update[TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD];
|
|
2119
|
+
if (position !== undefined && (kind !== "message" ||
|
|
2120
|
+
(position !== "comment" && position !== "forward"))) return { admitted: false };
|
|
2121
|
+
const carrier = update[kind];
|
|
2122
|
+
if (!Number.isSafeInteger(update.update_id) || update.update_id < 0 ||
|
|
2123
|
+
!isRecord(carrier) || carrier.pi_telegram_source_update_id !== update.update_id ||
|
|
2124
|
+
carrier.sender_chat !== undefined || carrier.actor_chat !== undefined) return { admitted: false };
|
|
2125
|
+
const sender = kind === "message_reaction" ? carrier.user : carrier.from;
|
|
2126
|
+
if (!isRecord(sender) || typeof sender.id !== "number" || !Number.isSafeInteger(sender.id) ||
|
|
2127
|
+
sender.id <= 0 || sender.is_bot !== false) return { admitted: false };
|
|
2128
|
+
return deps.configStore.withPairedUserAdmission(
|
|
2129
|
+
deps.profileName, deps.tokenSha256, sender.id, publish, deps.assertExecutionCurrent,
|
|
2130
|
+
);
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
|
|
1796
2134
|
export function prepareTelegramBusFollowerJournaledUpdateForExecution<
|
|
1797
2135
|
TUpdate extends { message?: unknown } & Record<string, unknown>,
|
|
1798
2136
|
>(
|
|
@@ -1832,6 +2170,9 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
|
|
|
1832
2170
|
if (!delivery) {
|
|
1833
2171
|
throw new Error("Telegram follower durable admission requires delivery identity.");
|
|
1834
2172
|
}
|
|
2173
|
+
if (envelope.kind === "leader.wakeInputCustody") throw new Error(
|
|
2174
|
+
"Telegram follower custody wake cannot use executable-copy admission.",
|
|
2175
|
+
);
|
|
1835
2176
|
const expected = createTelegramBusFollowerDeliveryIdentity({
|
|
1836
2177
|
kind: envelope.kind,
|
|
1837
2178
|
recipientBindingKey: delivery.recipientBindingKey,
|
|
@@ -1879,6 +2220,94 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
|
|
|
1879
2220
|
};
|
|
1880
2221
|
}
|
|
1881
2222
|
|
|
2223
|
+
export interface TelegramBusFollowerInputCustodyBundle<TContext> {
|
|
2224
|
+
acceptHandoff(input: {
|
|
2225
|
+
sourceRecoveryKey: string;
|
|
2226
|
+
recipientBindingKey: string;
|
|
2227
|
+
source: { journalBindingKey: string; tokenSha256: string; updateId: number };
|
|
2228
|
+
handoffId: string;
|
|
2229
|
+
}, ctx: TContext): unknown;
|
|
2230
|
+
wakeSource(input: TelegramBusFollowerDurableAdmissionResult & {
|
|
2231
|
+
recipientBindingKey: string;
|
|
2232
|
+
sourceRecoveryKey: string;
|
|
2233
|
+
sourceClaim: { acquisitionId: string; handoffId: string };
|
|
2234
|
+
}, ctx: TContext): void;
|
|
2235
|
+
resolveForwardReference(input: {
|
|
2236
|
+
sourceUpdateId: number; recipientBindingKey: string;
|
|
2237
|
+
}): { sourceRecoveryKey: string; source: { updateId: number; owner: {
|
|
2238
|
+
acquisitionId: string; handoffId: string } } } | undefined;
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
export function createTelegramBusFollowerInputCustodyPorts<TContext>(deps: {
|
|
2242
|
+
getInputCustodyBus(): TelegramBusFollowerInputCustodyBundle<TContext> | undefined;
|
|
2243
|
+
}) {
|
|
2244
|
+
const getRequired = (): TelegramBusFollowerInputCustodyBundle<TContext> => {
|
|
2245
|
+
const bundle = deps.getInputCustodyBus();
|
|
2246
|
+
if (!bundle) throw new Error("Telegram input custody bus binding is unavailable.");
|
|
2247
|
+
return bundle;
|
|
2248
|
+
};
|
|
2249
|
+
return {
|
|
2250
|
+
isSourceReferenceAdmissionEnabled: () => Boolean(deps.getInputCustodyBus()),
|
|
2251
|
+
handleInputCustodyHandoff(
|
|
2252
|
+
envelope: Extract<TelegramBusEnvelope, { kind: "leader.offerInputCustodyHandoff" }>,
|
|
2253
|
+
ctx: TContext,
|
|
2254
|
+
) {
|
|
2255
|
+
return getRequired().acceptHandoff({ sourceRecoveryKey: envelope.sourceRecoveryKey,
|
|
2256
|
+
recipientBindingKey: envelope.recipientBindingKey,
|
|
2257
|
+
source: envelope.source, handoffId: envelope.handoffId }, ctx);
|
|
2258
|
+
},
|
|
2259
|
+
sourceReferenceAdmission: createTelegramBusFollowerSourceReferenceAdmissionRuntime<TContext>({
|
|
2260
|
+
wakeSource(input, ctx) { getRequired().wakeSource(input, ctx); },
|
|
2261
|
+
}),
|
|
2262
|
+
resolveInputCustodyReference(input: {
|
|
2263
|
+
sourceUpdateId: number; recipientBindingKey: string;
|
|
2264
|
+
}) {
|
|
2265
|
+
return deps.getInputCustodyBus()?.resolveForwardReference(input);
|
|
2266
|
+
},
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
export function createTelegramBusFollowerSourceReferenceAdmissionRuntime<TContext>(deps: {
|
|
2271
|
+
wakeSource: (input: TelegramBusFollowerDurableAdmissionResult & {
|
|
2272
|
+
recipientBindingKey: string;
|
|
2273
|
+
sourceRecoveryKey: string;
|
|
2274
|
+
sourceClaim: { acquisitionId: string; handoffId: string };
|
|
2275
|
+
}, ctx: TContext) => Promise<void> | void;
|
|
2276
|
+
}): TelegramBusFollowerDurableAdmissionPort<TContext> {
|
|
2277
|
+
return {
|
|
2278
|
+
async admit(envelope, ctx) {
|
|
2279
|
+
const delivery = envelope.delivery;
|
|
2280
|
+
if (!delivery) throw new Error(
|
|
2281
|
+
"Telegram follower source-reference admission requires delivery identity.",
|
|
2282
|
+
);
|
|
2283
|
+
if (!delivery.sourceRecoveryKey) throw new Error(
|
|
2284
|
+
"Telegram follower source-reference admission requires a recovery key.",
|
|
2285
|
+
);
|
|
2286
|
+
if (!delivery.sourceClaim) throw new Error(
|
|
2287
|
+
"Telegram follower source-reference admission requires exact claim evidence.",
|
|
2288
|
+
);
|
|
2289
|
+
const expected = createTelegramBusFollowerDeliveryIdentity({ kind: envelope.kind,
|
|
2290
|
+
recipientBindingKey: delivery.recipientBindingKey,
|
|
2291
|
+
sourceUpdateId: delivery.sourceUpdateId });
|
|
2292
|
+
if (expected.deliveryId !== delivery.deliveryId) throw new Error(
|
|
2293
|
+
"Invalid Telegram follower source-reference delivery id.",
|
|
2294
|
+
);
|
|
2295
|
+
const carrier = envelope.kind === "leader.wakeInputCustody" ? undefined
|
|
2296
|
+
: envelope.kind === "leader.forwardCallback" ? envelope.query
|
|
2297
|
+
: envelope.kind === "leader.forwardReaction" ? envelope.reactionUpdate : envelope.message;
|
|
2298
|
+
if (envelope.kind !== "leader.wakeInputCustody" &&
|
|
2299
|
+
(!isRecord(carrier) || carrier.pi_telegram_source_update_id !== delivery.sourceUpdateId))
|
|
2300
|
+
throw new Error("Telegram follower source-reference update id mismatch.");
|
|
2301
|
+
await deps.wakeSource({ deliveryId: delivery.deliveryId,
|
|
2302
|
+
sourceUpdateId: delivery.sourceUpdateId,
|
|
2303
|
+
recipientBindingKey: delivery.recipientBindingKey,
|
|
2304
|
+
sourceRecoveryKey: delivery.sourceRecoveryKey,
|
|
2305
|
+
sourceClaim: { ...delivery.sourceClaim } }, ctx);
|
|
2306
|
+
return { deliveryId: delivery.deliveryId, sourceUpdateId: delivery.sourceUpdateId };
|
|
2307
|
+
},
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
|
|
1882
2311
|
export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
1883
2312
|
deps: TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>,
|
|
1884
2313
|
): TelegramBusForwardedUpdateReceiverRuntime {
|
|
@@ -1903,6 +2332,8 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
1903
2332
|
envelope.kind !== "leader.forwardReaction" &&
|
|
1904
2333
|
envelope.kind !== "leader.forwardMessage" &&
|
|
1905
2334
|
envelope.kind !== "leader.forwardEditedMessage" &&
|
|
2335
|
+
envelope.kind !== "leader.wakeInputCustody" &&
|
|
2336
|
+
envelope.kind !== "leader.offerInputCustodyHandoff" &&
|
|
1906
2337
|
envelope.kind !== "leader.replaceFollowerTarget" &&
|
|
1907
2338
|
envelope.kind !== "leader.offerQueueHandoff") ||
|
|
1908
2339
|
envelope.recipientInstanceId !== deps.instanceId
|
|
@@ -1926,9 +2357,15 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
1926
2357
|
message: "Stale Telegram bus follower registration generation.",
|
|
1927
2358
|
};
|
|
1928
2359
|
}
|
|
2360
|
+
if (envelope.kind === "leader.offerInputCustodyHandoff" &&
|
|
2361
|
+
envelope.recipientBindingKey !== deps.getRecipientBindingKey()) return {
|
|
2362
|
+
kind: "bus.ack", requestId: envelope.requestId, ok: false,
|
|
2363
|
+
message: "Mismatched Telegram follower handoff recipient identity.",
|
|
2364
|
+
};
|
|
1929
2365
|
if (
|
|
1930
2366
|
envelope.kind !== "leader.replaceFollowerTarget" &&
|
|
1931
2367
|
envelope.kind !== "leader.offerQueueHandoff" &&
|
|
2368
|
+
envelope.kind !== "leader.offerInputCustodyHandoff" &&
|
|
1932
2369
|
(!envelope.delivery ||
|
|
1933
2370
|
envelope.delivery.recipientBindingKey !==
|
|
1934
2371
|
deps.getRecipientBindingKey())
|
|
@@ -1950,11 +2387,35 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
1950
2387
|
};
|
|
1951
2388
|
}
|
|
1952
2389
|
try {
|
|
2390
|
+
if (envelope.kind === "leader.offerInputCustodyHandoff") {
|
|
2391
|
+
if (!(deps.isSourceReferenceAdmissionEnabled?.() ?? false)) throw new Error(
|
|
2392
|
+
"Telegram input custody handoff capability is not enabled.",
|
|
2393
|
+
);
|
|
2394
|
+
if (!deps.hasAuthenticatedSourceReferenceTransport?.()) throw new Error(
|
|
2395
|
+
"Telegram input custody handoff requires authenticated transport.",
|
|
2396
|
+
);
|
|
2397
|
+
if (!deps.handleInputCustodyHandoff) throw new Error(
|
|
2398
|
+
"Telegram input custody handoff acceptance is unavailable.",
|
|
2399
|
+
);
|
|
2400
|
+
const result = await deps.handleInputCustodyHandoff(envelope, ctx);
|
|
2401
|
+
return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
|
|
2402
|
+
}
|
|
1953
2403
|
if (
|
|
1954
2404
|
envelope.kind !== "leader.replaceFollowerTarget" &&
|
|
1955
2405
|
envelope.kind !== "leader.offerQueueHandoff"
|
|
1956
2406
|
) {
|
|
1957
|
-
const
|
|
2407
|
+
const sourceReferenceEnabled = envelope.kind === "leader.wakeInputCustody" ||
|
|
2408
|
+
(deps.isSourceReferenceAdmissionEnabled?.() ?? false);
|
|
2409
|
+
if (sourceReferenceEnabled &&
|
|
2410
|
+
!deps.hasAuthenticatedSourceReferenceTransport?.()) throw new Error(
|
|
2411
|
+
"Telegram follower source-reference admission requires authenticated transport.",
|
|
2412
|
+
);
|
|
2413
|
+
const admission = sourceReferenceEnabled
|
|
2414
|
+
? deps.sourceReferenceAdmission : deps.durableAdmission;
|
|
2415
|
+
if (!admission) throw new Error(
|
|
2416
|
+
"Telegram follower source-reference admission is enabled without a wake authority.",
|
|
2417
|
+
);
|
|
2418
|
+
const receipt = await admission.admit(envelope, ctx);
|
|
1958
2419
|
return {
|
|
1959
2420
|
kind: "bus.ack",
|
|
1960
2421
|
requestId: envelope.requestId,
|
|
@@ -2029,6 +2490,7 @@ function parseRegistrationResult(value: unknown): {
|
|
|
2029
2490
|
target?: TelegramTarget;
|
|
2030
2491
|
slot?: string;
|
|
2031
2492
|
threadName?: string;
|
|
2493
|
+
displayTitle?: string;
|
|
2032
2494
|
} {
|
|
2033
2495
|
if (!isRecord(value)) return {};
|
|
2034
2496
|
const target = parseTarget(isRecord(value.target) ? value.target : value);
|
|
@@ -2038,6 +2500,9 @@ function parseRegistrationResult(value: unknown): {
|
|
|
2038
2500
|
...(typeof value.threadName === "string"
|
|
2039
2501
|
? { threadName: value.threadName }
|
|
2040
2502
|
: {}),
|
|
2503
|
+
...(typeof value.displayTitle === "string"
|
|
2504
|
+
? { displayTitle: value.displayTitle }
|
|
2505
|
+
: {}),
|
|
2041
2506
|
};
|
|
2042
2507
|
}
|
|
2043
2508
|
|