@llblab/pi-telegram 0.51.4 → 0.51.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/BACKLOG.md +1 -0
- package/CHANGELOG.md +15 -0
- package/README.md +1 -1
- package/dist/lib/bindings.d.ts +4 -1
- package/dist/lib/bindings.js +16 -12
- package/dist/lib/bus-follower.d.ts +3 -1
- package/dist/lib/bus-follower.js +62 -3
- package/dist/lib/bus-leader.d.ts +21 -0
- package/dist/lib/bus-leader.js +144 -1
- package/dist/lib/bus.d.ts +9 -0
- package/dist/lib/bus.js +19 -0
- package/dist/lib/commands.d.ts +25 -3
- package/dist/lib/commands.js +166 -89
- package/dist/lib/extension.js +42 -0
- package/dist/lib/lifecycle.d.ts +57 -2
- package/dist/lib/lifecycle.js +109 -4
- package/dist/lib/locks.js +5 -1
- package/dist/lib/model.js +2 -4
- package/dist/lib/polling.js +6 -2
- package/dist/lib/status.d.ts +3 -1
- package/dist/lib/status.js +31 -1
- package/dist/lib/sync.js +4 -4
- package/dist/lib/threads.d.ts +8 -0
- package/dist/lib/threads.js +30 -9
- package/dist/lib/workspace-retirement.d.ts +2 -2
- package/dist/lib/workspace-retirement.js +48 -12
- package/dist/package.json +1 -1
- package/docs/architecture.md +6 -5
- package/docs/multi-instance-bus.md +4 -2
- package/docs/public-api.md +3 -3
- package/docs/ui-style.md +4 -0
- package/lib/bindings.ts +17 -10
- package/lib/bus-follower.ts +87 -3
- package/lib/bus-leader.ts +197 -0
- package/lib/bus.ts +33 -0
- package/lib/commands.ts +173 -103
- package/lib/extension.ts +40 -0
- package/lib/lifecycle.ts +140 -4
- package/lib/locks.ts +6 -1
- package/lib/model.ts +2 -4
- package/lib/polling.ts +10 -2
- package/lib/status.ts +30 -1
- package/lib/sync.ts +4 -4
- package/lib/threads.ts +40 -8
- package/lib/workspace-retirement.ts +46 -13
- package/package.json +1 -1
package/lib/bus-follower.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
49
49
|
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
50
50
|
TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
51
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
51
52
|
} from "./bus.ts";
|
|
52
53
|
import type { TelegramConfigStore, TelegramThreadDisplayMode } from "./config.ts";
|
|
53
54
|
import {
|
|
@@ -142,6 +143,11 @@ export interface TelegramBusFollowerRegistrationRuntime<TContext> {
|
|
|
142
143
|
target: TelegramTarget & { threadId: number },
|
|
143
144
|
) => Promise<string>;
|
|
144
145
|
setThreadDisplayMode?: (mode: TelegramThreadDisplayMode) => Promise<void>;
|
|
146
|
+
/** Leader-mediated durable publication or successor claim; true only after exact commit. */
|
|
147
|
+
requestSessionReplacement?: (
|
|
148
|
+
operation: "publish" | "settle",
|
|
149
|
+
intent: Threads.TelegramSessionReplacementIntent,
|
|
150
|
+
) => Promise<boolean>;
|
|
145
151
|
stop: () => void;
|
|
146
152
|
}
|
|
147
153
|
|
|
@@ -1124,10 +1130,16 @@ function isTelegramStaleContextError(error: unknown): boolean {
|
|
|
1124
1130
|
|
|
1125
1131
|
export function createTelegramBusFollowerSessionReplacementSuspender(
|
|
1126
1132
|
deps: TelegramBusFollowerSessionReplacementSuspenderDeps,
|
|
1127
|
-
): () => Promise<void> {
|
|
1133
|
+
): (preserveTarget?: boolean) => Promise<void> {
|
|
1128
1134
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
1129
1135
|
const getPid = deps.getPid ?? (() => process.pid);
|
|
1130
|
-
return async () => {
|
|
1136
|
+
return async (preserveTarget = true) => {
|
|
1137
|
+
if (!preserveTarget) {
|
|
1138
|
+
setTelegramFollowerSessionHandoff(undefined);
|
|
1139
|
+
Threads.setTelegramLeaderSessionHandoff(undefined);
|
|
1140
|
+
await deps.suspendPolling();
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1131
1143
|
const target = deps.registrationState.getTarget();
|
|
1132
1144
|
if (deps.registrationState.isRegistered() && target) {
|
|
1133
1145
|
setTelegramFollowerSessionHandoff({
|
|
@@ -1919,7 +1931,13 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
1919
1931
|
if (
|
|
1920
1932
|
registrationOptions?.restoreWorkspace &&
|
|
1921
1933
|
response.error?.code === "workspace-binding-unavailable"
|
|
1922
|
-
)
|
|
1934
|
+
) {
|
|
1935
|
+
deps.recordRuntimeEvent?.("bus", "Telegram follower auto-connect refused by leader: Workspace binding unavailable.", {
|
|
1936
|
+
phase: "follower-auto-connect-skip",
|
|
1937
|
+
reason: "leader-binding-unavailable",
|
|
1938
|
+
});
|
|
1939
|
+
return false;
|
|
1940
|
+
}
|
|
1923
1941
|
throw new Error(
|
|
1924
1942
|
response.message ??
|
|
1925
1943
|
"Telegram bus follower registration was rejected.",
|
|
@@ -2144,6 +2162,72 @@ export function createTelegramBusFollowerRegistrationRuntime<
|
|
|
2144
2162
|
}
|
|
2145
2163
|
return resetName;
|
|
2146
2164
|
},
|
|
2165
|
+
async requestSessionReplacement(operation, intent) {
|
|
2166
|
+
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
2167
|
+
throw new Error("Telegram follower is not registered with the leader.");
|
|
2168
|
+
}
|
|
2169
|
+
if (
|
|
2170
|
+
!hasTelegramBusCapability(
|
|
2171
|
+
deps.protocolIdentity,
|
|
2172
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
2173
|
+
) ||
|
|
2174
|
+
!hasTelegramBusCapability(
|
|
2175
|
+
deps.registrationState?.getLeaderProtocol(),
|
|
2176
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
2177
|
+
)
|
|
2178
|
+
) {
|
|
2179
|
+
throw new Error(
|
|
2180
|
+
"The active Telegram leader does not support follower session replacement. Update or restart that Pi instance.",
|
|
2181
|
+
);
|
|
2182
|
+
}
|
|
2183
|
+
const expectedLeaderSocketPath = activeLeaderSocketPath;
|
|
2184
|
+
const expectedRegistrationGeneration = activeRegistrationGeneration;
|
|
2185
|
+
const expectedAuthSecret = activeAuthSecret;
|
|
2186
|
+
const requestId = deps.createRequestId();
|
|
2187
|
+
const response = await sendTelegramBusLocalEnvelope({
|
|
2188
|
+
socketPath: expectedLeaderSocketPath,
|
|
2189
|
+
timeoutMs: registrationTimeoutMs,
|
|
2190
|
+
retry: getTelegramBusTransportRetryPolicy({
|
|
2191
|
+
endpoint: expectedLeaderSocketPath,
|
|
2192
|
+
operation: "operation",
|
|
2193
|
+
}),
|
|
2194
|
+
envelope: {
|
|
2195
|
+
kind: operation === "publish"
|
|
2196
|
+
? "follower.publishSessionReplacement"
|
|
2197
|
+
: "follower.settleSessionReplacement",
|
|
2198
|
+
requestId,
|
|
2199
|
+
auth: expectedAuthSecret,
|
|
2200
|
+
instanceId: deps.instanceId,
|
|
2201
|
+
registrationGeneration: expectedRegistrationGeneration,
|
|
2202
|
+
intent,
|
|
2203
|
+
sentAtMs: getNowMs(),
|
|
2204
|
+
},
|
|
2205
|
+
});
|
|
2206
|
+
if (
|
|
2207
|
+
response?.kind !== "bus.ack" || !response.ok ||
|
|
2208
|
+
response.requestId !== requestId ||
|
|
2209
|
+
!isRecord(response.result) || response.result.committed !== true
|
|
2210
|
+
) {
|
|
2211
|
+
throw new Error(
|
|
2212
|
+
response?.kind === "bus.ack"
|
|
2213
|
+
? (response.message ?? "Telegram session replacement was rejected.")
|
|
2214
|
+
: "Telegram session replacement was not acknowledged.",
|
|
2215
|
+
);
|
|
2216
|
+
}
|
|
2217
|
+
if (
|
|
2218
|
+
activeLeaderSocketPath !== expectedLeaderSocketPath ||
|
|
2219
|
+
activeRegistrationGeneration !== expectedRegistrationGeneration ||
|
|
2220
|
+
activeAuthSecret !== expectedAuthSecret ||
|
|
2221
|
+
(deps.registrationState &&
|
|
2222
|
+
deps.registrationState.getGeneration() !==
|
|
2223
|
+
expectedRegistrationGeneration)
|
|
2224
|
+
) {
|
|
2225
|
+
throw new Error(
|
|
2226
|
+
"Telegram session replacement completed for a stale follower registration.",
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
return true;
|
|
2230
|
+
},
|
|
2147
2231
|
async disconnectFromLeader() {
|
|
2148
2232
|
if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
|
|
2149
2233
|
return false;
|
package/lib/bus-leader.ts
CHANGED
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
|
|
43
43
|
TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
|
|
44
44
|
TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
|
|
45
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
45
46
|
} from "./bus.ts";
|
|
46
47
|
import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
|
|
47
48
|
import type { TelegramQueueHandoffPayload } from "./queue.ts";
|
|
@@ -273,6 +274,110 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
|
|
|
273
274
|
workspaceRotation?: TelegramWorkspaceSlotRotationPorts;
|
|
274
275
|
}
|
|
275
276
|
|
|
277
|
+
export type TelegramBusFollowerSessionReplacementOperation = (
|
|
278
|
+
follower: TelegramBusFollowerView,
|
|
279
|
+
intent: Threads.TelegramSessionReplacementIntent,
|
|
280
|
+
isCurrent: () => boolean,
|
|
281
|
+
) => Promise<boolean>;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Leader-owned durable session-replacement authority for registered followers.
|
|
285
|
+
* Followers cannot persist `state.json`; the leader validates the request
|
|
286
|
+
* against its live registry entry and authoritative Workspace binding before
|
|
287
|
+
* one CAS publication or claim. Follower memory is never accepted as binding
|
|
288
|
+
* evidence.
|
|
289
|
+
*/
|
|
290
|
+
export function createTelegramBusFollowerSessionReplacementAuthority(deps: {
|
|
291
|
+
store: Pick<
|
|
292
|
+
Threads.TelegramTopicTargetStore,
|
|
293
|
+
| "load"
|
|
294
|
+
| "getWorkspaceBindingByTarget"
|
|
295
|
+
| "getSessionReplacementIntent"
|
|
296
|
+
| "commitSessionReplacementIntent"
|
|
297
|
+
| "removeSessionReplacementIntent"
|
|
298
|
+
>;
|
|
299
|
+
getTelegramProfile?: () => string | undefined;
|
|
300
|
+
getNowMs?: () => number;
|
|
301
|
+
ttlMs?: number;
|
|
302
|
+
}): {
|
|
303
|
+
publish: TelegramBusFollowerSessionReplacementOperation;
|
|
304
|
+
settle: TelegramBusFollowerSessionReplacementOperation;
|
|
305
|
+
} {
|
|
306
|
+
const getNowMs = deps.getNowMs ?? Date.now;
|
|
307
|
+
const ttlMs = deps.ttlMs ?? Threads.TELEGRAM_LEADER_SESSION_HANDOFF_TTL_MS;
|
|
308
|
+
const assertCommon = (
|
|
309
|
+
follower: TelegramBusFollowerView,
|
|
310
|
+
intent: Threads.TelegramSessionReplacementIntent,
|
|
311
|
+
): { cwd: string; sessionId: string } => {
|
|
312
|
+
const cwd = follower.cwd
|
|
313
|
+
? Threads.normalizeTelegramWorkspacePath(follower.cwd)
|
|
314
|
+
: undefined;
|
|
315
|
+
const sessionId = follower.sessionId
|
|
316
|
+
? Threads.normalizeTelegramSessionId(follower.sessionId)
|
|
317
|
+
: undefined;
|
|
318
|
+
const nowMs = getNowMs();
|
|
319
|
+
if (
|
|
320
|
+
intent.continuity !== "workspace-thread" ||
|
|
321
|
+
typeof intent.target.threadId !== "number" ||
|
|
322
|
+
follower.target?.chatId !== intent.target.chatId ||
|
|
323
|
+
follower.target.threadId !== intent.target.threadId ||
|
|
324
|
+
!cwd || cwd !== intent.cwd || !sessionId ||
|
|
325
|
+
intent.profileName !== (deps.getTelegramProfile?.() ?? "default")
|
|
326
|
+
) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
"Telegram session replacement does not match the current follower registration.",
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
if (intent.expiresAtMs <= nowMs || intent.expiresAtMs > nowMs + ttlMs) {
|
|
332
|
+
throw new Error("Telegram session replacement intent is expired or unbounded.");
|
|
333
|
+
}
|
|
334
|
+
return { cwd, sessionId };
|
|
335
|
+
};
|
|
336
|
+
return {
|
|
337
|
+
async publish(follower, intent, isCurrent) {
|
|
338
|
+
const { sessionId } = assertCommon(follower, intent);
|
|
339
|
+
if (intent.sourceInstanceId !== follower.instanceId ||
|
|
340
|
+
intent.sourceSessionId !== sessionId) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
"Telegram session replacement source does not match the current follower registration.",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
await deps.store.load();
|
|
346
|
+
if (!isCurrent()) return false;
|
|
347
|
+
const binding = deps.store.getWorkspaceBindingByTarget(intent.target);
|
|
348
|
+
if (
|
|
349
|
+
!binding || binding.cwd !== intent.cwd ||
|
|
350
|
+
binding.sessionId !== intent.sourceSessionId ||
|
|
351
|
+
binding.slot !== intent.slot ||
|
|
352
|
+
(binding.manualThreadName ?? binding.threadName) !== intent.threadName
|
|
353
|
+
) {
|
|
354
|
+
throw new Error("Telegram session replacement binding is unavailable.");
|
|
355
|
+
}
|
|
356
|
+
return deps.store.commitSessionReplacementIntent(intent, isCurrent);
|
|
357
|
+
},
|
|
358
|
+
async settle(follower, intent, isCurrent) {
|
|
359
|
+
const { sessionId } = assertCommon(follower, intent);
|
|
360
|
+
if (
|
|
361
|
+
!intent.sourceInstanceId ||
|
|
362
|
+
(intent.sourceInstanceId !== follower.instanceId &&
|
|
363
|
+
intent.sourceInstanceId !== follower.previousInstanceId) ||
|
|
364
|
+
intent.sourceSessionId === sessionId
|
|
365
|
+
) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
"Telegram session replacement successor does not match the current follower registration.",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
await deps.store.load();
|
|
371
|
+
if (!isCurrent()) return false;
|
|
372
|
+
if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)?.cwd !==
|
|
373
|
+
intent.cwd) {
|
|
374
|
+
throw new Error("Telegram session replacement successor binding is unavailable.");
|
|
375
|
+
}
|
|
376
|
+
return deps.store.removeSessionReplacementIntent(intent, isCurrent);
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
276
381
|
export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
277
382
|
deps: TelegramBusLeaderRuntimeAssemblyDeps<TContext>,
|
|
278
383
|
): TelegramBusLeaderRuntime<TContext> & {
|
|
@@ -624,6 +729,12 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
624
729
|
}
|
|
625
730
|
});
|
|
626
731
|
});
|
|
732
|
+
const followerSessionReplacement =
|
|
733
|
+
createTelegramBusFollowerSessionReplacementAuthority({
|
|
734
|
+
store: deps.topicTargetStore,
|
|
735
|
+
getTelegramProfile: deps.getTelegramProfile,
|
|
736
|
+
getNowMs: deps.runtime.getNowMs,
|
|
737
|
+
});
|
|
627
738
|
const runtime = createTelegramBusLeaderRuntime({
|
|
628
739
|
...deps.runtime,
|
|
629
740
|
applyThreadDisplayMode,
|
|
@@ -750,6 +861,18 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
|
|
|
750
861
|
});
|
|
751
862
|
},
|
|
752
863
|
),
|
|
864
|
+
publishFollowerSessionReplacement: (follower, intent, isCurrent) =>
|
|
865
|
+
runWorkspaceOperation({
|
|
866
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
867
|
+
operationKind: "workspace.publish-follower-session-replacement",
|
|
868
|
+
scopes: [{ kind: "profile" }],
|
|
869
|
+
}, () => followerSessionReplacement.publish(follower, intent, isCurrent)),
|
|
870
|
+
settleFollowerSessionReplacement: (follower, intent, isCurrent) =>
|
|
871
|
+
runWorkspaceOperation({
|
|
872
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
873
|
+
operationKind: "workspace.settle-follower-session-replacement",
|
|
874
|
+
scopes: [{ kind: "profile" }],
|
|
875
|
+
}, () => followerSessionReplacement.settle(follower, intent, isCurrent)),
|
|
753
876
|
onFollowerConfirmedDead: async (follower) => {
|
|
754
877
|
await runWorkspaceOperation(
|
|
755
878
|
{
|
|
@@ -899,6 +1022,8 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
|
|
|
899
1022
|
resetFollowerThreadName?: (
|
|
900
1023
|
follower: TelegramBusFollowerView,
|
|
901
1024
|
) => Promise<{ threadName: string }> | { threadName: string };
|
|
1025
|
+
publishFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
1026
|
+
settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
902
1027
|
getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
|
|
903
1028
|
onFollowerRegistered?: () => void;
|
|
904
1029
|
applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
@@ -1923,6 +2048,8 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
1923
2048
|
resetFollowerThreadName?: (
|
|
1924
2049
|
follower: TelegramBusFollowerView,
|
|
1925
2050
|
) => Promise<{ threadName: string }> | { threadName: string };
|
|
2051
|
+
publishFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
2052
|
+
settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
|
|
1926
2053
|
getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
|
|
1927
2054
|
onFollowerRegistered?: () => void;
|
|
1928
2055
|
applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
|
|
@@ -2547,6 +2674,74 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
|
|
|
2547
2674
|
},
|
|
2548
2675
|
);
|
|
2549
2676
|
}
|
|
2677
|
+
case "follower.publishSessionReplacement":
|
|
2678
|
+
case "follower.settleSessionReplacement": {
|
|
2679
|
+
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
2680
|
+
return runFollowerMutation(
|
|
2681
|
+
registeredFollower ?? { instanceId: envelope.instanceId },
|
|
2682
|
+
async () => {
|
|
2683
|
+
const publish = envelope.kind === "follower.publishSessionReplacement";
|
|
2684
|
+
const operation = publish
|
|
2685
|
+
? deps.publishFollowerSessionReplacement
|
|
2686
|
+
: deps.settleFollowerSessionReplacement;
|
|
2687
|
+
const epoch = deps.getCurrentLeaderEpoch?.();
|
|
2688
|
+
const follower = deps.followerRegistry.get(envelope.instanceId);
|
|
2689
|
+
const isCurrent = () => epoch !== undefined &&
|
|
2690
|
+
deps.getCurrentLeaderEpoch?.() === epoch &&
|
|
2691
|
+
deps.followerRegistry.get(envelope.instanceId)?.registrationGeneration ===
|
|
2692
|
+
envelope.registrationGeneration;
|
|
2693
|
+
if (
|
|
2694
|
+
!follower?.registrationGeneration ||
|
|
2695
|
+
follower.registrationGeneration !== envelope.registrationGeneration ||
|
|
2696
|
+
!isCurrent() || !operation ||
|
|
2697
|
+
!hasTelegramBusCapability(
|
|
2698
|
+
deps.protocolIdentity,
|
|
2699
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
2700
|
+
) ||
|
|
2701
|
+
!hasTelegramBusCapability(
|
|
2702
|
+
follower.protocol,
|
|
2703
|
+
TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
|
|
2704
|
+
)
|
|
2705
|
+
) {
|
|
2706
|
+
return {
|
|
2707
|
+
kind: "bus.ack" as const,
|
|
2708
|
+
requestId: envelope.requestId,
|
|
2709
|
+
ok: false,
|
|
2710
|
+
message:
|
|
2711
|
+
"Telegram session replacement requires current follower registration and compatible leader authority.",
|
|
2712
|
+
};
|
|
2713
|
+
}
|
|
2714
|
+
try {
|
|
2715
|
+
const committed = await operation(follower, envelope.intent, isCurrent);
|
|
2716
|
+
if (!committed || !isCurrent()) {
|
|
2717
|
+
return {
|
|
2718
|
+
kind: "bus.ack" as const,
|
|
2719
|
+
requestId: envelope.requestId,
|
|
2720
|
+
ok: false,
|
|
2721
|
+
message: publish
|
|
2722
|
+
? "Telegram session replacement intent was not persisted."
|
|
2723
|
+
: "Telegram session replacement intent was not claimed.",
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
return {
|
|
2727
|
+
kind: "bus.ack" as const,
|
|
2728
|
+
requestId: envelope.requestId,
|
|
2729
|
+
ok: true,
|
|
2730
|
+
result: { committed: true },
|
|
2731
|
+
};
|
|
2732
|
+
} catch (error) {
|
|
2733
|
+
return {
|
|
2734
|
+
kind: "bus.ack" as const,
|
|
2735
|
+
requestId: envelope.requestId,
|
|
2736
|
+
ok: false,
|
|
2737
|
+
message: error instanceof Error
|
|
2738
|
+
? error.message
|
|
2739
|
+
: "Telegram session replacement request failed.",
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
},
|
|
2743
|
+
);
|
|
2744
|
+
}
|
|
2550
2745
|
case "follower.disconnect": {
|
|
2551
2746
|
const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
|
|
2552
2747
|
return runFollowerMutation(
|
|
@@ -3118,6 +3313,8 @@ export function createTelegramBusLeaderRuntime<TContext>(
|
|
|
3118
3313
|
onFollowerDisconnected: deps.onFollowerDisconnected,
|
|
3119
3314
|
renameFollowerThread: deps.renameFollowerThread,
|
|
3120
3315
|
resetFollowerThreadName: deps.resetFollowerThreadName,
|
|
3316
|
+
publishFollowerSessionReplacement: deps.publishFollowerSessionReplacement,
|
|
3317
|
+
settleFollowerSessionReplacement: deps.settleFollowerSessionReplacement,
|
|
3121
3318
|
getFollowerDisplayTitle: deps.getFollowerDisplayTitle,
|
|
3122
3319
|
onFollowerRegistered: deps.onFollowerRegistered,
|
|
3123
3320
|
applyThreadDisplayMode: deps.applyThreadDisplayMode,
|
package/lib/bus.ts
CHANGED
|
@@ -52,6 +52,10 @@ import type { TelegramTarget } from "./target.ts";
|
|
|
52
52
|
import type { TelegramThreadDisplayMode } from "./config.ts";
|
|
53
53
|
import { isProcessAlive } from "./locks.ts";
|
|
54
54
|
import { resolveAgentDir } from "./paths.ts";
|
|
55
|
+
import {
|
|
56
|
+
normalizeTelegramSessionReplacementIntent,
|
|
57
|
+
type TelegramSessionReplacementIntent,
|
|
58
|
+
} from "./threads.ts";
|
|
55
59
|
|
|
56
60
|
export interface TelegramBusProcessRuntime {
|
|
57
61
|
instanceId: string;
|
|
@@ -234,6 +238,8 @@ export const TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT =
|
|
|
234
238
|
"directory-display-format-v1" as const;
|
|
235
239
|
export const TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT =
|
|
236
240
|
"workspace-follower-auto-connect-v1" as const;
|
|
241
|
+
export const TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT =
|
|
242
|
+
"session-replacement-intent-v1" as const;
|
|
237
243
|
|
|
238
244
|
export interface TelegramBusProtocolIdentity {
|
|
239
245
|
protocolVersion: number;
|
|
@@ -816,6 +822,16 @@ export type TelegramBusEnvelope = (
|
|
|
816
822
|
target: TelegramTarget & { threadId: number };
|
|
817
823
|
sentAtMs: number;
|
|
818
824
|
}
|
|
825
|
+
| {
|
|
826
|
+
kind:
|
|
827
|
+
| "follower.publishSessionReplacement"
|
|
828
|
+
| "follower.settleSessionReplacement";
|
|
829
|
+
requestId: string;
|
|
830
|
+
instanceId: string;
|
|
831
|
+
registrationGeneration: string;
|
|
832
|
+
intent: TelegramSessionReplacementIntent;
|
|
833
|
+
sentAtMs: number;
|
|
834
|
+
}
|
|
819
835
|
| {
|
|
820
836
|
kind: "leader.forwardCallback";
|
|
821
837
|
requestId: string;
|
|
@@ -1055,6 +1071,23 @@ export function parseTelegramBusEnvelope(
|
|
|
1055
1071
|
}
|
|
1056
1072
|
break;
|
|
1057
1073
|
}
|
|
1074
|
+
case "follower.publishSessionReplacement":
|
|
1075
|
+
case "follower.settleSessionReplacement": {
|
|
1076
|
+
const intent = normalizeTelegramSessionReplacementIntent(value.intent);
|
|
1077
|
+
if (typeof value.instanceId === "string" &&
|
|
1078
|
+
typeof value.registrationGeneration === "string" &&
|
|
1079
|
+
typeof value.sentAtMs === "number" && intent) {
|
|
1080
|
+
envelope = {
|
|
1081
|
+
kind,
|
|
1082
|
+
requestId,
|
|
1083
|
+
instanceId: value.instanceId,
|
|
1084
|
+
registrationGeneration: value.registrationGeneration,
|
|
1085
|
+
intent,
|
|
1086
|
+
sentAtMs: value.sentAtMs,
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
break;
|
|
1090
|
+
}
|
|
1058
1091
|
case "leader.forwardCallback":
|
|
1059
1092
|
envelope = parseForwardCallbackEnvelope(value, requestId);
|
|
1060
1093
|
break;
|