@llblab/pi-kit 0.6.0 → 0.7.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 +8 -0
- package/README.md +4 -4
- 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-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 +23 -5
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
- 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 +227 -23
- 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 +6 -6
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -6
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
- package/node_modules/@llblab/pi-telegram/index.ts +4 -1418
- package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +46 -1
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +32 -19
- 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 +355 -26
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +718 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +237 -11
- package/node_modules/@llblab/pi-telegram/lib/config.ts +242 -26
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1851 -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/locks.ts +44 -2
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +152 -13
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +97 -10
- 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 +17 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +6 -2
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +118 -26
- package/node_modules/@llblab/pi-telegram/lib/rendering.ts +4 -1
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +21 -2
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +344 -112
- package/node_modules/@llblab/pi-telegram/lib/setup.ts +44 -4
- package/node_modules/@llblab/pi-telegram/lib/status.ts +51 -4
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +353 -22
- 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/turns.ts +7 -0
- 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/package.json +2 -3
- package/package.json +6 -5
- /package/node_modules/@llblab/pi-telegram/lib/{logs.ts → logging.ts} +0 -0
|
@@ -7,13 +7,24 @@
|
|
|
7
7
|
import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from "./telegram-api.ts";
|
|
8
8
|
import { getTelegramTargetKey, type TelegramTarget } from "./target.ts";
|
|
9
9
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
10
|
+
import {
|
|
11
|
+
createTelegramWorkspaceAdmissionOperationId,
|
|
12
|
+
runWithTelegramWorkspaceAdmissionsAsync,
|
|
13
|
+
type TelegramWorkspaceAdmissionLedger,
|
|
14
|
+
} from "./workspace-admission.ts";
|
|
10
15
|
import {
|
|
11
16
|
createTelegramCleanupTargetProtection,
|
|
17
|
+
commitTelegramWorkspaceProvisionBinding,
|
|
12
18
|
getTelegramTargetFromApiBody,
|
|
19
|
+
getTelegramThreadOwnerKey,
|
|
20
|
+
isSameTelegramProcessInstance,
|
|
13
21
|
isTelegramTopicTargetStaleError,
|
|
22
|
+
normalizeTelegramWorkspacePath,
|
|
14
23
|
provisionOwnBusTopic,
|
|
15
24
|
type TelegramOwnTopicProvisionResult,
|
|
16
25
|
type TelegramTopicTargetStore,
|
|
26
|
+
type TelegramWorkspaceDisplayBinding,
|
|
27
|
+
type TelegramWorkspaceThreadBinding,
|
|
17
28
|
} from "./threads.ts";
|
|
18
29
|
|
|
19
30
|
export interface TelegramTopicLifecycleSyncUpdate<TMessage = unknown> {
|
|
@@ -22,12 +33,25 @@ export interface TelegramTopicLifecycleSyncUpdate<TMessage = unknown> {
|
|
|
22
33
|
message: TMessage;
|
|
23
34
|
}
|
|
24
35
|
|
|
36
|
+
export type TelegramSyncWorkspaceOperationRunner = <T>(
|
|
37
|
+
input: {
|
|
38
|
+
operationId: string;
|
|
39
|
+
operationKind: string;
|
|
40
|
+
scopes: readonly [{ kind: "profile" }];
|
|
41
|
+
},
|
|
42
|
+
operation: () => Promise<T>,
|
|
43
|
+
) => Promise<T>;
|
|
44
|
+
|
|
25
45
|
export interface TelegramLeaderThreadSyncDeps {
|
|
26
46
|
getAllowedUserId: () => number | undefined;
|
|
27
47
|
instanceId: string;
|
|
28
48
|
cwd?: string;
|
|
29
49
|
telegramProfile?: string;
|
|
30
50
|
forceFreshUnnamed?: boolean;
|
|
51
|
+
requestedThreadName?: string;
|
|
52
|
+
resolveInitialWorkspaceDisplayTitle?: (
|
|
53
|
+
binding: TelegramWorkspaceDisplayBinding,
|
|
54
|
+
) => string | undefined;
|
|
31
55
|
getNowMs?: () => number;
|
|
32
56
|
getRandom?: () => number;
|
|
33
57
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
@@ -41,6 +65,9 @@ export interface TelegramLeaderThreadSyncDeps {
|
|
|
41
65
|
method: string,
|
|
42
66
|
body: Record<string, unknown>,
|
|
43
67
|
) => Promise<TResponse>;
|
|
68
|
+
probeWorkspaceBinding?: (
|
|
69
|
+
binding: TelegramWorkspaceThreadBinding,
|
|
70
|
+
) => Promise<void>;
|
|
44
71
|
recordEvent: (
|
|
45
72
|
category: string,
|
|
46
73
|
message: unknown,
|
|
@@ -87,6 +114,7 @@ export type TelegramTopicLifecycleSyncHandler<TMessage = unknown> = (
|
|
|
87
114
|
export interface TelegramObservedTopicLifecycleSyncDeps<
|
|
88
115
|
TSyncState,
|
|
89
116
|
> extends TelegramTopicLifecycleSyncDeps {
|
|
117
|
+
runWorkspaceOperation: TelegramSyncWorkspaceOperationRunner;
|
|
90
118
|
getSyncState: () => TSyncState;
|
|
91
119
|
setSyncState: (state: TSyncState) => void;
|
|
92
120
|
getNowMs?: () => number;
|
|
@@ -144,6 +172,8 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
|
|
|
144
172
|
error: unknown,
|
|
145
173
|
details?: Record<string, unknown>,
|
|
146
174
|
) => void;
|
|
175
|
+
runWorkspaceOperation: TelegramSyncWorkspaceOperationRunner;
|
|
176
|
+
workspaceOperationKind?: string;
|
|
147
177
|
getNowMs?: () => number;
|
|
148
178
|
}
|
|
149
179
|
|
|
@@ -179,6 +209,7 @@ export function createTelegramSessionRestartThreadCleanupHandler<
|
|
|
179
209
|
): () => Promise<string> {
|
|
180
210
|
return createTelegramManualThreadDisconnectHandler({
|
|
181
211
|
...deps,
|
|
212
|
+
workspaceOperationKind: "workspace.cleanup-session-restart",
|
|
182
213
|
async stopPolling() {
|
|
183
214
|
await deps.suspendPolling();
|
|
184
215
|
return "Telegram bridge suspended for session restart.";
|
|
@@ -202,6 +233,7 @@ export function createTelegramThreadDisconnectAssembly<
|
|
|
202
233
|
return {
|
|
203
234
|
disconnect: createTelegramManualThreadDisconnectHandler({
|
|
204
235
|
...deps,
|
|
236
|
+
workspaceOperationKind: "workspace.disconnect-thread",
|
|
205
237
|
stopPolling: deps.stopPolling,
|
|
206
238
|
}),
|
|
207
239
|
cleanupForSessionRestart:
|
|
@@ -215,7 +247,7 @@ export function createTelegramThreadDisconnectAssembly<
|
|
|
215
247
|
export function createTelegramManualThreadDisconnectHandler<
|
|
216
248
|
TSyncState extends TelegramSyncState,
|
|
217
249
|
>(deps: TelegramManualThreadDisconnectDeps<TSyncState>): () => Promise<string> {
|
|
218
|
-
|
|
250
|
+
const operation = async (): Promise<string> => {
|
|
219
251
|
const currentRecord = deps.getCurrentThreadRecord();
|
|
220
252
|
let cleanupPending = false;
|
|
221
253
|
if (currentRecord?.target.threadId) {
|
|
@@ -308,6 +340,14 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
308
340
|
? `${stopped} Telegram thread cleanup remains pending for the next leader.`
|
|
309
341
|
: stopped;
|
|
310
342
|
};
|
|
343
|
+
return () => deps.runWorkspaceOperation(
|
|
344
|
+
{
|
|
345
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
346
|
+
operationKind: deps.workspaceOperationKind ?? "workspace.disconnect-thread",
|
|
347
|
+
scopes: [{ kind: "profile" }],
|
|
348
|
+
},
|
|
349
|
+
operation,
|
|
350
|
+
);
|
|
311
351
|
}
|
|
312
352
|
|
|
313
353
|
export function createTelegramLeaderHealthRuntime<
|
|
@@ -425,6 +465,10 @@ export interface TelegramStaleTopicApiErrorRecoveryDeps<TSyncState> {
|
|
|
425
465
|
getNowMs?: () => number;
|
|
426
466
|
isCurrent?: () => boolean;
|
|
427
467
|
isAuthorityCurrent?: () => boolean;
|
|
468
|
+
getWorkspaceAdmission?: () => Pick<
|
|
469
|
+
TelegramWorkspaceAdmissionLedger,
|
|
470
|
+
"acquireAdmission" | "releaseAdmission"
|
|
471
|
+
> | undefined;
|
|
428
472
|
}
|
|
429
473
|
|
|
430
474
|
export function captureTelegramStaleTargetRequestRecovery<TSyncState extends TelegramSyncState>(
|
|
@@ -498,40 +542,81 @@ export async function recoverStaleTelegramTopicApiError<
|
|
|
498
542
|
deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState>,
|
|
499
543
|
): Promise<boolean> {
|
|
500
544
|
const target = getTelegramTargetFromApiBody(apiBody);
|
|
501
|
-
if (
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
545
|
+
if (
|
|
546
|
+
!target ||
|
|
547
|
+
!isTelegramTopicTargetStaleError(error) ||
|
|
548
|
+
deps.isCurrent?.() === false
|
|
549
|
+
) return false;
|
|
550
|
+
const recover = async (): Promise<boolean> => {
|
|
551
|
+
if (deps.isCurrent) {
|
|
552
|
+
if (
|
|
553
|
+
!deps.topicTargetStore.invalidateTarget ||
|
|
554
|
+
!await deps.topicTargetStore.invalidateTarget(
|
|
555
|
+
target,
|
|
556
|
+
deps.isCurrent,
|
|
557
|
+
String(error),
|
|
558
|
+
)
|
|
559
|
+
) return false;
|
|
560
|
+
if (deps.isAuthorityCurrent?.() === false) return false;
|
|
561
|
+
} else {
|
|
562
|
+
await deps.topicTargetStore.load();
|
|
563
|
+
if (
|
|
564
|
+
!deps.topicTargetStore.markStaleByTarget(
|
|
565
|
+
target,
|
|
566
|
+
"deleted",
|
|
567
|
+
String(error),
|
|
568
|
+
)
|
|
569
|
+
) return false;
|
|
570
|
+
await deps.topicTargetStore.persist();
|
|
571
|
+
}
|
|
572
|
+
const nowMs = (deps.getNowMs ?? Date.now)();
|
|
573
|
+
let state = markTelegramSyncSliceSuspect(
|
|
574
|
+
deps.getSyncState(),
|
|
575
|
+
"topic-state",
|
|
576
|
+
{
|
|
577
|
+
nowMs,
|
|
578
|
+
reason: "stale-api-error",
|
|
579
|
+
action: "topic-target-stale",
|
|
580
|
+
},
|
|
581
|
+
) as TSyncState;
|
|
582
|
+
state = markTelegramSyncSliceSuspect(state, "transport-health", {
|
|
583
|
+
nowMs,
|
|
584
|
+
reason: "stale-api-error",
|
|
585
|
+
action: "topic-target-stale",
|
|
586
|
+
}) as TSyncState;
|
|
587
|
+
state = markTelegramSyncSliceSuspect(state, "target-bindings", {
|
|
588
|
+
nowMs,
|
|
589
|
+
reason: "stale-api-error",
|
|
590
|
+
action: "topic-target-stale",
|
|
591
|
+
}) as TSyncState;
|
|
592
|
+
deps.setSyncState(state);
|
|
593
|
+
deps.recordEvent("bus", error, {
|
|
594
|
+
phase: "topic-target-stale",
|
|
595
|
+
chatId: target.chatId,
|
|
596
|
+
threadId: target.threadId,
|
|
597
|
+
});
|
|
598
|
+
return true;
|
|
599
|
+
};
|
|
600
|
+
if (!deps.getWorkspaceAdmission) return recover();
|
|
601
|
+
const admission = deps.getWorkspaceAdmission();
|
|
602
|
+
if (!admission) {
|
|
603
|
+
throw new Error("Telegram Workspace admission authority is unavailable.");
|
|
511
604
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
action: "topic-target-stale",
|
|
527
|
-
}) as TSyncState;
|
|
528
|
-
deps.setSyncState(state);
|
|
529
|
-
deps.recordEvent("bus", error, {
|
|
530
|
-
phase: "topic-target-stale",
|
|
531
|
-
chatId: target.chatId,
|
|
532
|
-
threadId: target.threadId,
|
|
605
|
+
return runWithTelegramWorkspaceAdmissionsAsync({
|
|
606
|
+
ledger: admission,
|
|
607
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
608
|
+
operationKind: "workspace.recover-stale-target",
|
|
609
|
+
scopes: [{ kind: "target", target }],
|
|
610
|
+
operation: recover,
|
|
611
|
+
onReleaseError(releaseError) {
|
|
612
|
+
deps.recordEvent("bus", releaseError, {
|
|
613
|
+
phase: "workspace-admission-release",
|
|
614
|
+
operationKind: "workspace.recover-stale-target",
|
|
615
|
+
chatId: target.chatId,
|
|
616
|
+
threadId: target.threadId,
|
|
617
|
+
});
|
|
618
|
+
},
|
|
533
619
|
});
|
|
534
|
-
return true;
|
|
535
620
|
}
|
|
536
621
|
|
|
537
622
|
export async function ensureTelegramLeaderThreadBinding(
|
|
@@ -552,6 +637,54 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
552
637
|
assertLeaderEpoch("start");
|
|
553
638
|
await deps.topicTargetStore.load();
|
|
554
639
|
assertLeaderEpoch("after-load");
|
|
640
|
+
const normalizedLeaderCwd = deps.cwd
|
|
641
|
+
? normalizeTelegramWorkspacePath(deps.cwd)
|
|
642
|
+
: undefined;
|
|
643
|
+
const leaderOwner = {
|
|
644
|
+
kind: "leader" as const,
|
|
645
|
+
cwd: normalizedLeaderCwd,
|
|
646
|
+
instanceId: deps.instanceId,
|
|
647
|
+
...(deps.telegramProfile ? { telegramProfile: deps.telegramProfile } : {}),
|
|
648
|
+
};
|
|
649
|
+
const leaderProfileKey = getTelegramThreadOwnerKey(leaderOwner);
|
|
650
|
+
const legacyLeaderRecord =
|
|
651
|
+
deps.topicTargetStore.getByProfileKey(leaderProfileKey);
|
|
652
|
+
const workspaceIdentity = normalizedLeaderCwd
|
|
653
|
+
? deps.topicTargetStore.claimWorkspaceIdentity(
|
|
654
|
+
normalizedLeaderCwd,
|
|
655
|
+
deps.instanceId,
|
|
656
|
+
legacyLeaderRecord?.instanceId,
|
|
657
|
+
)
|
|
658
|
+
: undefined;
|
|
659
|
+
if (deps.cwd && !workspaceIdentity) {
|
|
660
|
+
throw new Error("Telegram Workspace identity is already claimed.");
|
|
661
|
+
}
|
|
662
|
+
const commitWorkspaceBinding = async (
|
|
663
|
+
result: TelegramOwnTopicProvisionResult,
|
|
664
|
+
): Promise<TelegramOwnTopicProvisionResult> => {
|
|
665
|
+
if (!workspaceIdentity) return result;
|
|
666
|
+
const committed = commitTelegramWorkspaceProvisionBinding({
|
|
667
|
+
store: deps.topicTargetStore,
|
|
668
|
+
instanceId: deps.instanceId,
|
|
669
|
+
profileKey: leaderProfileKey,
|
|
670
|
+
displayTitle: result.displayTitle,
|
|
671
|
+
binding: {
|
|
672
|
+
...workspaceIdentity,
|
|
673
|
+
target: { ...result.target },
|
|
674
|
+
...(result.threadName ? { threadName: result.threadName } : {}),
|
|
675
|
+
...(result.slot ? { slot: result.slot } : {}),
|
|
676
|
+
journalBindingKeys: [],
|
|
677
|
+
journalBindingsComplete: true,
|
|
678
|
+
updatedAtMs: deps.getNowMs?.() ?? Date.now(),
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
deps.topicTargetStore.markWorkspaceBindingActiveByTarget(result.target);
|
|
682
|
+
assertLeaderEpoch("before-workspace-persist");
|
|
683
|
+
await deps.topicTargetStore.persist();
|
|
684
|
+
assertLeaderEpoch("after-workspace-persist");
|
|
685
|
+
return { ...result, ...(committed.displayTitle ? { displayTitle: committed.displayTitle } : {}) };
|
|
686
|
+
};
|
|
687
|
+
try {
|
|
555
688
|
const unavailableTargetKeys = new Set([
|
|
556
689
|
...deps.topicTargetStore
|
|
557
690
|
.listSyncObservations()
|
|
@@ -578,6 +711,120 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
578
711
|
await deps.topicTargetStore.persist();
|
|
579
712
|
assertLeaderEpoch("after-unavailable-persist");
|
|
580
713
|
}
|
|
714
|
+
const persistedWorkspaceBinding = workspaceIdentity
|
|
715
|
+
? deps.topicTargetStore.getWorkspaceBinding(
|
|
716
|
+
workspaceIdentity.cwd,
|
|
717
|
+
workspaceIdentity.instanceSlot,
|
|
718
|
+
)
|
|
719
|
+
: undefined;
|
|
720
|
+
const legacyWorkspaceBinding =
|
|
721
|
+
workspaceIdentity?.instanceSlot === "a" &&
|
|
722
|
+
!persistedWorkspaceBinding &&
|
|
723
|
+
typeof legacyLeaderRecord?.target.threadId === "number"
|
|
724
|
+
? {
|
|
725
|
+
...workspaceIdentity,
|
|
726
|
+
target: { ...legacyLeaderRecord.target },
|
|
727
|
+
...(legacyLeaderRecord.threadName
|
|
728
|
+
? { threadName: legacyLeaderRecord.threadName }
|
|
729
|
+
: {}),
|
|
730
|
+
...(legacyLeaderRecord.slot ? { slot: legacyLeaderRecord.slot } : {}),
|
|
731
|
+
updatedAtMs: legacyLeaderRecord.updatedAtMs,
|
|
732
|
+
}
|
|
733
|
+
: undefined;
|
|
734
|
+
const recoverableWorkspaceBinding =
|
|
735
|
+
persistedWorkspaceBinding ?? legacyWorkspaceBinding;
|
|
736
|
+
const recoverableTargetKey = recoverableWorkspaceBinding
|
|
737
|
+
? getTelegramTargetKey(recoverableWorkspaceBinding.target)
|
|
738
|
+
: undefined;
|
|
739
|
+
const recoverableRecord = recoverableWorkspaceBinding
|
|
740
|
+
? deps.topicTargetStore
|
|
741
|
+
.list()
|
|
742
|
+
.find(
|
|
743
|
+
(record) =>
|
|
744
|
+
getTelegramTargetKey(record.target) === recoverableTargetKey,
|
|
745
|
+
)
|
|
746
|
+
: undefined;
|
|
747
|
+
const sameProcessWorkspaceBinding =
|
|
748
|
+
!!recoverableRecord &&
|
|
749
|
+
(recoverableRecord.instanceId === deps.instanceId ||
|
|
750
|
+
isSameTelegramProcessInstance(
|
|
751
|
+
recoverableRecord.instanceId,
|
|
752
|
+
deps.instanceId,
|
|
753
|
+
));
|
|
754
|
+
if (
|
|
755
|
+
recoverableWorkspaceBinding &&
|
|
756
|
+
!unavailableTargetKeys.has(recoverableTargetKey ?? "") &&
|
|
757
|
+
(!deps.forceFreshUnnamed || recoverableWorkspaceBinding.threadName) &&
|
|
758
|
+
(sameProcessWorkspaceBinding || deps.probeWorkspaceBinding)
|
|
759
|
+
) {
|
|
760
|
+
let workspaceTargetVisible = sameProcessWorkspaceBinding;
|
|
761
|
+
if (!workspaceTargetVisible && deps.probeWorkspaceBinding) {
|
|
762
|
+
assertLeaderEpoch("before-workspace-probe");
|
|
763
|
+
try {
|
|
764
|
+
await deps.probeWorkspaceBinding(recoverableWorkspaceBinding);
|
|
765
|
+
assertLeaderEpoch("after-workspace-probe");
|
|
766
|
+
workspaceTargetVisible = true;
|
|
767
|
+
} catch (error) {
|
|
768
|
+
if (!isTelegramTopicTargetStaleError(error)) throw error;
|
|
769
|
+
deps.topicTargetStore.markStaleByTarget(
|
|
770
|
+
recoverableWorkspaceBinding.target,
|
|
771
|
+
"deleted",
|
|
772
|
+
error instanceof Error ? error.message : String(error),
|
|
773
|
+
);
|
|
774
|
+
await deps.topicTargetStore.persist();
|
|
775
|
+
deps.recordEvent("bus", error, {
|
|
776
|
+
phase: "leader-workspace-target-stale",
|
|
777
|
+
chatId: recoverableWorkspaceBinding.target.chatId,
|
|
778
|
+
threadId: recoverableWorkspaceBinding.target.threadId,
|
|
779
|
+
instanceId: deps.instanceId,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (workspaceTargetVisible) {
|
|
784
|
+
const nowMs = deps.getNowMs?.() ?? Date.now();
|
|
785
|
+
const slot = workspaceIdentity?.slot;
|
|
786
|
+
if (!slot) {
|
|
787
|
+
throw new Error("Telegram Thread slot authority is unavailable.");
|
|
788
|
+
}
|
|
789
|
+
const recovered = await commitWorkspaceBinding({
|
|
790
|
+
target: { ...recoverableWorkspaceBinding.target },
|
|
791
|
+
slot,
|
|
792
|
+
...(recoverableWorkspaceBinding.threadName
|
|
793
|
+
? { threadName: recoverableWorkspaceBinding.threadName }
|
|
794
|
+
: {}),
|
|
795
|
+
reused: true,
|
|
796
|
+
});
|
|
797
|
+
deps.topicTargetStore.upsert({
|
|
798
|
+
profileKey: leaderProfileKey,
|
|
799
|
+
owner: leaderOwner,
|
|
800
|
+
target: { ...recovered.target },
|
|
801
|
+
status: "active",
|
|
802
|
+
createdAtMs: recoverableRecord?.createdAtMs ?? nowMs,
|
|
803
|
+
updatedAtMs: nowMs,
|
|
804
|
+
...(recovered.threadName ? { threadName: recovered.threadName } : {}),
|
|
805
|
+
instanceId: deps.instanceId,
|
|
806
|
+
slot: recovered.slot,
|
|
807
|
+
syncStatus: "open",
|
|
808
|
+
lastSyncObservedAtMs: nowMs,
|
|
809
|
+
lastReconcileAction: "leader-workspace-binding-recovered",
|
|
810
|
+
});
|
|
811
|
+
assertLeaderEpoch("before-recovered-record-persist");
|
|
812
|
+
await deps.topicTargetStore.persist();
|
|
813
|
+
assertLeaderEpoch("after-recovered-record-persist");
|
|
814
|
+
deps.recordEvent(
|
|
815
|
+
"telegram",
|
|
816
|
+
"Leader thread preserved after Workspace recovery",
|
|
817
|
+
{
|
|
818
|
+
phase: "leader-thread-reused",
|
|
819
|
+
instanceId: deps.instanceId,
|
|
820
|
+
chatId: recovered.target.chatId,
|
|
821
|
+
threadId: recovered.target.threadId,
|
|
822
|
+
slot: recovered.slot,
|
|
823
|
+
},
|
|
824
|
+
);
|
|
825
|
+
return recovered;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
581
828
|
const priorTargets = deps.topicTargetStore.list().filter((record) => {
|
|
582
829
|
return (
|
|
583
830
|
record.instanceId === deps.instanceId &&
|
|
@@ -601,12 +848,15 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
601
848
|
},
|
|
602
849
|
);
|
|
603
850
|
assertLeaderEpoch("before-reuse");
|
|
604
|
-
|
|
851
|
+
if (!record.slot) {
|
|
852
|
+
throw new Error("Telegram Thread slot authority is unavailable.");
|
|
853
|
+
}
|
|
854
|
+
return await commitWorkspaceBinding({
|
|
605
855
|
target: record.target,
|
|
606
|
-
slot: record.slot
|
|
856
|
+
slot: record.slot,
|
|
607
857
|
...(record.threadName ? { threadName: record.threadName } : {}),
|
|
608
858
|
reused: true,
|
|
609
|
-
};
|
|
859
|
+
});
|
|
610
860
|
}
|
|
611
861
|
let forcedUnnamedStale = false;
|
|
612
862
|
if (deps.forceFreshUnnamed) {
|
|
@@ -635,7 +885,7 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
635
885
|
const ownTarget = await provisionOwnBusTopic({
|
|
636
886
|
getAllowedUserId: deps.getAllowedUserId,
|
|
637
887
|
instanceId: deps.instanceId,
|
|
638
|
-
cwd:
|
|
888
|
+
cwd: normalizedLeaderCwd,
|
|
639
889
|
telegramProfile: deps.telegramProfile,
|
|
640
890
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
641
891
|
getThreadReconciliationMachineState:
|
|
@@ -645,6 +895,12 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
645
895
|
callApi: deps.callApi,
|
|
646
896
|
getNowMs: deps.getNowMs,
|
|
647
897
|
getRandom: deps.getRandom,
|
|
898
|
+
requestedThreadName:
|
|
899
|
+
recoverableWorkspaceBinding?.threadName ?? deps.requestedThreadName,
|
|
900
|
+
workspaceBindingKey: workspaceIdentity?.bindingKey,
|
|
901
|
+
preferredSlot: workspaceIdentity?.slot,
|
|
902
|
+
resolveInitialWorkspaceDisplayTitle:
|
|
903
|
+
deps.resolveInitialWorkspaceDisplayTitle,
|
|
648
904
|
recordEvent: deps.recordEvent,
|
|
649
905
|
});
|
|
650
906
|
assertLeaderEpoch("after-provision");
|
|
@@ -681,7 +937,10 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
681
937
|
assertLeaderEpoch("before-final-persist");
|
|
682
938
|
await deps.topicTargetStore.persist();
|
|
683
939
|
assertLeaderEpoch("after-final-persist");
|
|
684
|
-
return ownTarget;
|
|
940
|
+
return await commitWorkspaceBinding(ownTarget);
|
|
941
|
+
} finally {
|
|
942
|
+
deps.topicTargetStore.releaseWorkspaceClaim(deps.instanceId);
|
|
943
|
+
}
|
|
685
944
|
}
|
|
686
945
|
|
|
687
946
|
export const TELEGRAM_SYNC_SLICE_TARGET_BINDINGS = "target-bindings";
|
|
@@ -855,7 +1114,9 @@ export function createTelegramObservedTopicLifecycleSyncHandler<
|
|
|
855
1114
|
): TelegramTopicLifecycleSyncHandler<TMessage> {
|
|
856
1115
|
const syncTopicLifecycle =
|
|
857
1116
|
createTelegramTopicLifecycleSyncHandler<TMessage>(deps);
|
|
858
|
-
|
|
1117
|
+
const operation = async (
|
|
1118
|
+
lifecycle: TelegramTopicLifecycleSyncUpdate<TMessage>,
|
|
1119
|
+
): Promise<void> => {
|
|
859
1120
|
const nowMs = deps.getNowMs ?? Date.now;
|
|
860
1121
|
deps.assertExecutionCurrent?.(lifecycle.message);
|
|
861
1122
|
deps.setSyncState(
|
|
@@ -874,6 +1135,14 @@ export function createTelegramObservedTopicLifecycleSyncHandler<
|
|
|
874
1135
|
}) as TSyncState,
|
|
875
1136
|
);
|
|
876
1137
|
};
|
|
1138
|
+
return (lifecycle) => deps.runWorkspaceOperation(
|
|
1139
|
+
{
|
|
1140
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
1141
|
+
operationKind: "workspace.sync-topic-lifecycle",
|
|
1142
|
+
scopes: [{ kind: "profile" }],
|
|
1143
|
+
},
|
|
1144
|
+
() => operation(lifecycle),
|
|
1145
|
+
);
|
|
877
1146
|
}
|
|
878
1147
|
|
|
879
1148
|
export function createTelegramTopicLifecycleSyncHandler<TMessage = unknown>(
|