@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.
Files changed (141) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +5 -5
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
  35. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  36. package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
  37. package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
  38. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  39. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  40. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  41. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  42. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  43. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  44. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  45. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  46. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  47. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  50. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  51. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  55. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  56. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  59. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  63. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  64. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  65. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  66. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  68. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  69. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  70. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  71. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  72. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  73. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  74. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  75. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  76. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  77. package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
  78. package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -4
  79. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +22 -5
  80. package/node_modules/@llblab/pi-telegram/README.md +13 -9
  81. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  82. package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
  83. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  84. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  85. package/node_modules/@llblab/pi-telegram/docs/outbound.md +13 -7
  86. package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -5
  87. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  88. package/node_modules/@llblab/pi-telegram/index.ts +4 -1415
  89. package/node_modules/@llblab/pi-telegram/lib/activity.ts +19 -5
  90. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  91. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +37 -2
  92. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  93. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  94. package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
  95. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
  96. package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
  97. package/node_modules/@llblab/pi-telegram/lib/config.ts +178 -25
  98. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
  99. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
  100. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  101. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  102. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -1
  103. package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
  104. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +154 -15
  105. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +74 -40
  106. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +28 -42
  107. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +18 -14
  108. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  109. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  110. package/node_modules/@llblab/pi-telegram/lib/preview.ts +115 -70
  111. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
  112. package/node_modules/@llblab/pi-telegram/lib/queue.ts +66 -22
  113. package/node_modules/@llblab/pi-telegram/lib/replies.ts +47 -39
  114. package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
  115. package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
  116. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  117. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +315 -7
  118. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  119. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  120. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  121. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  122. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  123. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  124. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  125. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  126. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  127. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  128. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  129. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  130. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  131. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  132. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  133. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
  134. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
  135. package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
  136. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
  137. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
  138. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
  139. package/node_modules/@llblab/skills/package.json +2 -3
  140. package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
  141. package/package.json +7 -6
@@ -207,6 +207,7 @@ export interface TelegramBridgeInboundWorkerState {
207
207
  phaseStartedAtMs?: number;
208
208
  currentUpdateId?: number;
209
209
  blockedReason?: string;
210
+ blockedInputCustody?: { updateId: number; kind: string };
210
211
  journalEntryCount: number;
211
212
  journalSerializedBytes: number;
212
213
  oldestAdmittedAtMs?: number;
@@ -294,6 +295,7 @@ export interface TelegramStatusBarState {
294
295
  pollingStopReason?: string;
295
296
  paired: boolean;
296
297
  busRole?: TelegramBridgeBusRole;
298
+ followerRegistered?: boolean;
297
299
  busLifecyclePhase?: TelegramBridgeBusLifecyclePhase;
298
300
  instanceSlot?: string;
299
301
  instanceThreadName?: string;
@@ -677,11 +679,13 @@ export function createTelegramBridgeStatusRuntime<
677
679
  const hasPendingModelSwitch = deps.hasPendingModelSwitch();
678
680
  const activeToolExecutions = deps.getActiveToolExecutions();
679
681
  const compactionInProgress = deps.isCompactionInProgress();
682
+ const localBus = deps.getLocalBus?.();
680
683
  return {
681
684
  hasBotToken: !!config.botToken,
682
685
  pollingActive: deps.isPollingActive(),
683
686
  paired: !!config.allowedUserId,
684
687
  busRole: deps.getBusRole?.(),
688
+ followerRegistered: localBus?.followerRegistered,
685
689
  busLifecyclePhase: deps.getBusLifecyclePhase?.(),
686
690
  instanceSlot: deps.getInstanceSlot?.(),
687
691
  instanceThreadName: deps.getInstanceThreadName?.(),
@@ -902,6 +906,9 @@ export function buildTelegramStatusBarText(
902
906
  if (state.error) {
903
907
  return `${label} ${theme.fg("error", "error")}`;
904
908
  }
909
+ if (state.busRole === "follower" && state.followerRegistered === false) {
910
+ return `${label} ${theme.fg("warning", "reconnecting")}${queued}`;
911
+ }
905
912
  if (state.processing) {
906
913
  const processingStatus = state.queuedStatus
907
914
  ? "active"
@@ -1277,6 +1284,9 @@ function buildTelegramInboundWorkerDiagnosticLines(
1277
1284
  ...(worker.blockedReason
1278
1285
  ? [`- blocked reason: ${worker.blockedReason}`]
1279
1286
  : []),
1287
+ ...(worker.blockedInputCustody
1288
+ ? [`- blocked input custody: update=${worker.blockedInputCustody.updateId}, kind=${worker.blockedInputCustody.kind}`]
1289
+ : []),
1280
1290
  ...(worker.lastCompletedUpdateId !== undefined
1281
1291
  ? [
1282
1292
  `- last completed: ${worker.lastCompletedUpdateId}${worker.lastCompletedAtMs !== undefined ? ` at ${new Date(worker.lastCompletedAtMs).toISOString()}` : ""}`,
@@ -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
- return async () => {
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 (!target || !isTelegramTopicTargetStaleError(error) || deps.isCurrent?.() === false) return false;
502
- if (deps.isCurrent) {
503
- if (!deps.topicTargetStore.invalidateTarget || !await deps.topicTargetStore.invalidateTarget(
504
- target, deps.isCurrent, String(error),
505
- )) return false;
506
- if (deps.isAuthorityCurrent?.() === false) return false;
507
- } else {
508
- await deps.topicTargetStore.load();
509
- if (!deps.topicTargetStore.markStaleByTarget(target, "deleted", String(error))) return false;
510
- await deps.topicTargetStore.persist();
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
- const nowMs = (deps.getNowMs ?? Date.now)();
513
- let state = markTelegramSyncSliceSuspect(deps.getSyncState(), "topic-state", {
514
- nowMs,
515
- reason: "stale-api-error",
516
- action: "topic-target-stale",
517
- }) as TSyncState;
518
- state = markTelegramSyncSliceSuspect(state, "transport-health", {
519
- nowMs,
520
- reason: "stale-api-error",
521
- action: "topic-target-stale",
522
- }) as TSyncState;
523
- state = markTelegramSyncSliceSuspect(state, "target-bindings", {
524
- nowMs,
525
- reason: "stale-api-error",
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
- return {
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 ?? "A",
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: deps.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
- return async (lifecycle) => {
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>(