@llblab/pi-kit 0.27.6 → 0.28.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 (182) hide show
  1. package/BACKLOG.md +2 -5
  2. package/CHANGELOG.md +5 -0
  3. package/README.md +2 -2
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  7. package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
  8. package/node_modules/@llblab/pi-telegram/README.md +15 -5
  9. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  10. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
  12. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
  13. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
  14. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
  16. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
  17. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
  18. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
  19. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
  20. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
  21. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
  22. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
  23. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
  24. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
  25. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
  26. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
  27. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
  28. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
  29. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
  30. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
  31. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
  32. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
  33. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
  34. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
  35. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
  36. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
  37. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
  38. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
  39. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
  40. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
  41. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
  42. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
  43. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
  44. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
  45. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
  46. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
  47. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
  48. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
  49. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
  50. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
  51. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
  52. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
  53. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
  54. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
  55. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
  56. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
  57. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
  58. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
  59. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
  60. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
  61. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
  62. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
  63. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
  64. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
  65. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
  66. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
  67. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
  68. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
  69. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
  70. package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
  71. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
  72. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
  73. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
  74. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
  75. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
  76. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
  77. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
  78. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
  79. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
  80. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
  81. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
  82. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
  83. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
  84. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
  85. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
  86. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
  87. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
  88. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
  89. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
  90. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
  91. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
  92. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
  93. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
  94. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
  95. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
  96. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
  97. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
  98. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
  99. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
  100. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
  101. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
  102. package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
  103. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
  104. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
  105. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
  106. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
  107. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
  108. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
  109. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
  110. package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
  111. package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
  112. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  113. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  114. package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
  115. package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
  116. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  117. package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
  118. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
  119. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  120. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
  121. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
  122. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  123. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  124. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
  125. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
  126. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
  127. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
  128. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
  129. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
  130. package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
  131. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
  132. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
  133. package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
  134. package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
  135. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
  136. package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
  137. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
  138. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
  139. package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
  140. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
  141. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
  142. package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
  143. package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
  144. package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
  145. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
  146. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
  147. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
  148. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
  149. package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
  150. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
  151. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
  152. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
  153. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
  154. package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
  155. package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
  156. package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
  157. package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
  158. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
  159. package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
  160. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
  161. package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
  162. package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
  163. package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
  164. package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
  165. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  166. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
  167. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
  168. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
  169. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
  170. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
  171. package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
  172. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
  173. package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
  174. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
  175. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
  176. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
  177. package/node_modules/@llblab/pi-telegram/package.json +6 -6
  178. package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
  179. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
  180. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
  181. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
  182. package/package.json +2 -2
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Non-coercing decoded wire-value inspection
3
+ * Zones: wire fields, record shape, integer bounds
4
+ * Owns shared shallow predicates; excludes schemas, lossless decoding,
5
+ * required fields, normalization, storage and execution authority.
6
+ */
7
+
8
+ /** Non-array object shape only; not a plain-object or JSON-serializability guarantee. */
9
+ export function isWireRecord(value: unknown): value is Record<string, unknown> {
10
+ return !!value && typeof value === "object" && !Array.isArray(value);
11
+ }
12
+
13
+ /** Checks only own enumerable string keys, not required, inherited or symbol fields. */
14
+ export function hasOnlyWireKeys(
15
+ value: Record<string, unknown>,
16
+ allowedKeys: readonly string[],
17
+ ): boolean {
18
+ const allowed = new Set(allowedKeys);
19
+ return Object.keys(value).every(key => allowed.has(key));
20
+ }
21
+
22
+ export function isNonEmptyWireString(value: unknown): value is string {
23
+ return typeof value === "string" && value.length > 0;
24
+ }
25
+
26
+ export function isNonNegativeWireInteger(value: unknown): value is number {
27
+ return Number.isSafeInteger(value) && (value as number) >= 0;
28
+ }
@@ -15,6 +15,7 @@ import {
15
15
  type PathLike,
16
16
  } from "node:fs";
17
17
  import { dirname } from "node:path";
18
+ import { isDeepStrictEqual } from "node:util";
18
19
 
19
20
  import {
20
21
  getTelegramProcessLiveness,
@@ -23,8 +24,12 @@ import {
23
24
  import {
24
25
  renameTelegramPathWithRetry,
25
26
  withTelegramFileTransaction,
27
+ readTelegramRuntimeState,
28
+ mutateTelegramRuntimeStateSection,
29
+ TelegramRuntimeStateError,
26
30
  } from "./locks.ts";
27
- import type { TelegramTarget } from "./target.ts";
31
+ import { areTelegramTargetsEqual as areTargetsEqual, type TelegramTarget } from "./target.ts";
32
+ import { isWireRecord as isRecord, isNonNegativeWireInteger as isSafeTimestamp } from "./wire.ts";
28
33
 
29
34
  const TELEGRAM_WORKSPACE_ADMISSION_VERSION = 1;
30
35
  const TELEGRAM_WORKSPACE_ADMISSION_MAX_LEASES = 4096;
@@ -41,7 +46,7 @@ export type TelegramWorkspaceAdmissionScope =
41
46
  | { kind: "chat"; chatId: number }
42
47
  | { kind: "profile" };
43
48
 
44
- export interface TelegramWorkspaceAdmissionLease {
49
+ interface TelegramWorkspaceAdmissionLease {
45
50
  operationId: string;
46
51
  operationKind: string;
47
52
  profileKey: string;
@@ -50,7 +55,7 @@ export interface TelegramWorkspaceAdmissionLease {
50
55
  acquiredAtMs: number;
51
56
  }
52
57
 
53
- export type TelegramWorkspaceDestructiveFenceKind =
58
+ type TelegramWorkspaceDestructiveFenceKind =
54
59
  | "pressure-retirement"
55
60
  | "manual-thread-cleanup"
56
61
  | "journal-writer-closure";
@@ -58,7 +63,7 @@ export type TelegramWorkspaceDestructiveFenceKind =
58
63
  export type TelegramWorkspaceDeletionFenceKind = Exclude<
59
64
  TelegramWorkspaceDestructiveFenceKind, "journal-writer-closure">;
60
65
 
61
- export interface TelegramWorkspaceJournalWriterClosureFence {
66
+ interface TelegramWorkspaceJournalWriterClosureFence {
62
67
  destructiveKind: "journal-writer-closure";
63
68
  phase: "fenced";
64
69
  operationId: string;
@@ -87,7 +92,7 @@ export function normalizeTelegramWorkspaceJournalWriterClosureFence(
87
92
  owner, requestedAtMs: value.requestedAtMs, acquiredAtMs: value.acquiredAtMs };
88
93
  }
89
94
 
90
- export interface TelegramWorkspaceJournalWriterProtocolMode {
95
+ interface TelegramWorkspaceJournalWriterProtocolMode {
91
96
  version: 1;
92
97
  protocol: "custody-v3";
93
98
  profileKey: string;
@@ -183,16 +188,11 @@ interface TelegramWorkspaceAdmissionState {
183
188
  writerProtocolMode?: TelegramWorkspaceJournalWriterProtocolMode;
184
189
  }
185
190
 
186
- export type TelegramWorkspaceAdmissionBlockReason =
187
- | "retirement-fenced"
188
- | "admission-active"
189
- | "retirement-active";
190
-
191
- export type TelegramWorkspaceAdmissionAcquireResult =
191
+ type TelegramWorkspaceAdmissionAcquireResult =
192
192
  | { kind: "acquired"; lease: TelegramWorkspaceAdmissionLease; resumed: boolean }
193
193
  | { kind: "blocked"; reason: "retirement-fenced" };
194
194
 
195
- export type TelegramWorkspaceRetirementFenceAcquireResult =
195
+ type TelegramWorkspaceRetirementFenceAcquireResult =
196
196
  | {
197
197
  kind: "acquired";
198
198
  fence: TelegramWorkspaceRetirementFence;
@@ -203,16 +203,16 @@ export type TelegramWorkspaceRetirementFenceAcquireResult =
203
203
  reason: "admission-active" | "retirement-active";
204
204
  };
205
205
 
206
- export type TelegramWorkspaceJournalWriterProtocolInstallResult = {
206
+ type TelegramWorkspaceJournalWriterProtocolInstallResult = {
207
207
  mode: TelegramWorkspaceJournalWriterProtocolMode;
208
208
  resumed: boolean;
209
209
  };
210
210
 
211
- export type TelegramWorkspaceJournalWriterClosureAcquireResult =
211
+ type TelegramWorkspaceJournalWriterClosureAcquireResult =
212
212
  | { kind: "acquired"; fence: TelegramWorkspaceJournalWriterClosureFence; resumed: boolean }
213
213
  | { kind: "blocked"; reason: "admission-active" | "retirement-active" };
214
214
 
215
- export type TelegramWorkspaceDeletionPermitResult =
215
+ type TelegramWorkspaceDeletionPermitResult =
216
216
  | {
217
217
  kind: "issued";
218
218
  fence: TelegramWorkspaceRetirementFence;
@@ -220,19 +220,21 @@ export type TelegramWorkspaceDeletionPermitResult =
220
220
  }
221
221
  | { kind: "already-issued"; fence: TelegramWorkspaceRetirementFence };
222
222
 
223
- export interface TelegramWorkspaceAdmissionLedgerSnapshot {
223
+ interface TelegramWorkspaceAdmissionLedgerSnapshot {
224
224
  profileKey: string;
225
225
  leases: TelegramWorkspaceAdmissionLease[];
226
226
  fence?: TelegramWorkspaceDestructiveFence;
227
227
  writerProtocolMode?: TelegramWorkspaceJournalWriterProtocolMode;
228
228
  }
229
229
 
230
- export type TelegramWorkspaceAdmissionPublicationBoundary =
230
+ type TelegramWorkspaceAdmissionPublicationBoundary =
231
231
  | "before-write"
232
232
  | "after-write-before-rename";
233
233
 
234
- export interface TelegramWorkspaceAdmissionLedgerOptions {
234
+ interface TelegramWorkspaceAdmissionLedgerOptions {
235
235
  path: string;
236
+ /** Explicit logical section identity for the consolidated root; absent means the standalone ledger adapter. */
237
+ stateProfile?: string;
236
238
  profileKey: string;
237
239
  owner: TelegramWorkspaceAdmissionOwner;
238
240
  getNowMs?: () => number;
@@ -356,14 +358,6 @@ function authorityChanged(message: string): never {
356
358
  throw new TelegramWorkspaceAdmissionError("authority-changed", message);
357
359
  }
358
360
 
359
- function isRecord(value: unknown): value is Record<string, unknown> {
360
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
361
- }
362
-
363
- function isSafeTimestamp(value: unknown): value is number {
364
- return Number.isSafeInteger(value) && (value as number) >= 0;
365
- }
366
-
367
361
  function isBoundedText(value: unknown): value is string {
368
362
  return (
369
363
  typeof value === "string" &&
@@ -508,8 +502,9 @@ function normalizeFence(
508
502
  if (value.phase === "deletion-rejected" &&
509
503
  (value.destructiveKind === undefined || value.destructiveKind === "pressure-retirement") &&
510
504
  isSafeTimestamp(value.deletionIssuedAtMs) && isSafeTimestamp(value.rejectionConfirmedAtMs)) {
511
- return { ...base, destructiveKind: value.destructiveKind, phase: "deletion-rejected",
512
- deletionIssuedAtMs: value.deletionIssuedAtMs, rejectionConfirmedAtMs: value.rejectionConfirmedAtMs };
505
+ const { destructiveKind: _kind, ...pressureBase } = base;
506
+ return { ...pressureBase, ...(value.destructiveKind === "pressure-retirement" ? { destructiveKind: "pressure-retirement" as const } : {}),
507
+ phase: "deletion-rejected", deletionIssuedAtMs: value.deletionIssuedAtMs, rejectionConfirmedAtMs: value.rejectionConfirmedAtMs };
513
508
  }
514
509
  if (
515
510
  value.phase === "commit-ready" &&
@@ -587,9 +582,29 @@ function readState(
587
582
  } catch (error) {
588
583
  invalidState("Telegram Workspace admission state is malformed.", error);
589
584
  }
590
- if (!isRecord(parsed)) {
591
- invalidState("Telegram Workspace admission state is invalid.");
592
- }
585
+ return normalizeAdmissionState(parsed, profileKey);
586
+ }
587
+
588
+ function isConsolidatedAdmissionKey(value: unknown, profile: string): boolean {
589
+ if (!isBoundedText(value)) return false;
590
+ try {
591
+ const key: unknown = JSON.parse(value);
592
+ return isRecord(key) && Object.keys(key).length === 3 && key.version === TELEGRAM_WORKSPACE_ADMISSION_VERSION && key.profile === profile &&
593
+ isRecord(key.bot) && Object.keys(key.bot).length === 1 && typeof key.bot.tokenSha256 === "string" && /^[a-f0-9]{64}$/u.test(key.bot.tokenSha256);
594
+ } catch { return false; }
595
+ }
596
+ /** Pure damage check for one consolidated admission section under its own stored profile key; throws when invalid. */
597
+ export function assertTelegramConsolidatedAdmissionSection(value: unknown, stateProfile: string): void {
598
+ if (value === undefined) return;
599
+ normalizeAdmissionState(value, isRecord(value) && typeof value.profileKey === "string" ? value.profileKey : "", stateProfile);
600
+ }
601
+
602
+ function normalizeAdmissionState(parsed: unknown, profileKey: string, stateProfile?: string): TelegramWorkspaceAdmissionState {
603
+ if (parsed === undefined) return { version: TELEGRAM_WORKSPACE_ADMISSION_VERSION, profileKey, leases: [] };
604
+ if (!isRecord(parsed)) invalidState("Telegram Workspace admission state is invalid.");
605
+ if (stateProfile !== undefined && (Object.keys(parsed).some(key => !["version", "profileKey", "leases", "fence", "writerProtocolMode"].includes(key)) ||
606
+ !isConsolidatedAdmissionKey(parsed.profileKey, stateProfile)))
607
+ invalidState("Telegram consolidated admission section identity is invalid.");
593
608
  if (
594
609
  parsed.version !== TELEGRAM_WORKSPACE_ADMISSION_VERSION ||
595
610
  !Array.isArray(parsed.leases) ||
@@ -627,6 +642,9 @@ function readState(
627
642
  invalidState("Telegram Workspace writer protocol mode is invalid.");
628
643
  if (fence && writerProtocolMode)
629
644
  invalidState("Telegram Workspace destructive fence conflicts with writer protocol mode.");
645
+ if (stateProfile !== undefined && (!isDeepStrictEqual(parsed.leases, normalizedLeases) ||
646
+ !isDeepStrictEqual(parsed.fence, fence) || !isDeepStrictEqual(parsed.writerProtocolMode, writerProtocolMode)))
647
+ invalidState("Telegram consolidated admission evidence cannot drop unknown fields.");
630
648
  return {
631
649
  version: TELEGRAM_WORKSPACE_ADMISSION_VERSION,
632
650
  profileKey,
@@ -679,13 +697,6 @@ function writeState(
679
697
  }
680
698
  }
681
699
 
682
- function areTargetsEqual(
683
- left: TelegramTarget & { threadId: number },
684
- right: TelegramTarget & { threadId: number },
685
- ): boolean {
686
- return left.chatId === right.chatId && left.threadId === right.threadId;
687
- }
688
-
689
700
  function scopesConflict(
690
701
  scope: TelegramWorkspaceAdmissionScope,
691
702
  target: TelegramTarget & { threadId: number },
@@ -810,6 +821,8 @@ function validateOptions(options: TelegramWorkspaceAdmissionLedgerOptions): void
810
821
  if (!normalizeOwner(options.owner)) {
811
822
  invalidInput("Telegram Workspace admission owner is invalid.");
812
823
  }
824
+ if (options.stateProfile !== undefined && (!isBoundedText(options.stateProfile) || !isConsolidatedAdmissionKey(options.profileKey, options.stateProfile)))
825
+ invalidInput("Telegram consolidated admission requires an exact bot/profile section identity.");
813
826
  }
814
827
 
815
828
  function validateOperationId(operationId: string): void {
@@ -863,31 +876,34 @@ export function createTelegramWorkspaceAdmissionProfileKey(input: {
863
876
  });
864
877
  }
865
878
 
866
- export interface TelegramWorkspaceAdmissionRuntimeBinding {
879
+ interface TelegramWorkspaceAdmissionRuntimeBinding {
867
880
  resolve: () => TelegramWorkspaceAdmissionLedger | undefined;
868
881
  }
869
882
 
870
883
  export function createTelegramWorkspaceAdmissionRuntimeBinding(input: {
871
884
  getProfileName: () => string | undefined;
872
885
  getBotToken: () => string | undefined;
873
- getPath: (profileName?: string) => string;
874
886
  owner: TelegramWorkspaceAdmissionOwner;
875
887
  getNowMs?: () => number;
876
888
  getProcessLiveness?: (
877
889
  owner: TelegramWorkspaceAdmissionOwner,
878
890
  ) => TelegramProcessLiveness;
879
- }): TelegramWorkspaceAdmissionRuntimeBinding {
891
+ } & ({ getPath: (profileName?: string) => string; getStatePath?: never } |
892
+ { getStatePath: () => string; getPath?: never })): TelegramWorkspaceAdmissionRuntimeBinding {
893
+ const { getProfileName, getBotToken, getPath, getStatePath } = input;
894
+ if ((getPath === undefined) === (getStatePath === undefined)) invalidInput("Telegram admission binding must select one storage identity.");
880
895
  return {
881
896
  resolve() {
882
- const botToken = input.getBotToken();
897
+ const botToken = getBotToken();
883
898
  if (!botToken) return undefined;
884
- const configuredProfileName = input.getProfileName();
899
+ const configuredProfileName = getProfileName();
885
900
  const profileKey = createTelegramWorkspaceAdmissionProfileKey({
886
901
  profileName: configuredProfileName ?? "default",
887
902
  botToken,
888
903
  });
889
904
  return createTelegramWorkspaceAdmissionLedger({
890
- path: input.getPath(configuredProfileName),
905
+ path: getStatePath ? getStatePath() : getPath!(configuredProfileName),
906
+ ...(getStatePath ? { stateProfile: configuredProfileName ?? "default" } : {}),
891
907
  profileKey,
892
908
  owner: input.owner,
893
909
  getNowMs: input.getNowMs,
@@ -1105,12 +1121,23 @@ export function createTelegramWorkspaceAdmissionLedger(
1105
1121
  options: TelegramWorkspaceAdmissionLedgerOptions,
1106
1122
  ): TelegramWorkspaceAdmissionLedger {
1107
1123
  validateOptions(options);
1124
+ const suppliedOptions = options;
1125
+ if (options.stateProfile !== undefined) options = { ...options, owner: { ...options.owner } };
1108
1126
  const getNowMs = options.getNowMs ?? Date.now;
1109
1127
  const getProcessLiveness =
1110
1128
  options.getProcessLiveness ??
1111
1129
  ((owner: TelegramWorkspaceAdmissionOwner) =>
1112
1130
  getTelegramProcessLiveness(owner));
1113
1131
  const transactionPath = `${options.path}.transaction`;
1132
+ const path = options.path, profileKey = options.profileKey, stateProfile = options.stateProfile, boundOwner = { ...options.owner };
1133
+ const publication = { publishRename: options.publishRename, onPublicationBoundary: options.onPublicationBoundary };
1134
+ const isStorageCurrent = (): boolean => suppliedOptions.path === path && suppliedOptions.profileKey === profileKey &&
1135
+ suppliedOptions.stateProfile === stateProfile && areOwnersEqual(suppliedOptions.owner, boundOwner);
1136
+ const mapStateError = (error: unknown): never => {
1137
+ if (error instanceof TelegramRuntimeStateError) throw new TelegramWorkspaceAdmissionError(
1138
+ error.code === "invalid" ? "invalid-state" : error.code, "Telegram consolidated admission state operation failed.", { cause: error });
1139
+ throw error;
1140
+ };
1114
1141
 
1115
1142
  function transact<T>(
1116
1143
  operation: (state: TelegramWorkspaceAdmissionState) => {
@@ -1118,6 +1145,20 @@ export function createTelegramWorkspaceAdmissionLedger(
1118
1145
  changed: boolean;
1119
1146
  },
1120
1147
  ): T {
1148
+ if (stateProfile !== undefined) {
1149
+ try {
1150
+ return mutateTelegramRuntimeStateSection(path, stateProfile, "admission", current => {
1151
+ const state = normalizeAdmissionState(current, profileKey, stateProfile);
1152
+ const retained = state.leases.filter(lease => getProcessLiveness(lease.owner) !== "dead");
1153
+ const pruned = retained.length !== state.leases.length;
1154
+ state.leases = retained;
1155
+ const outcome = operation(state);
1156
+ return { value: pruned || outcome.changed ? normalizeAdmissionState(state, profileKey, stateProfile) : current, result: outcome.result };
1157
+ }, { isCurrent: isStorageCurrent, publishRename: publication.publishRename, onPublicationBoundary(boundary) {
1158
+ if (boundary !== "after-rename") publication.onPublicationBoundary?.(boundary, path);
1159
+ } });
1160
+ } catch (error) { return mapStateError(error); }
1161
+ }
1121
1162
  return withTelegramFileTransaction(transactionPath, () => {
1122
1163
  const state = readState(options.path, options.profileKey);
1123
1164
  const retainedLeases = state.leases.filter(
@@ -1132,7 +1173,16 @@ export function createTelegramWorkspaceAdmissionLedger(
1132
1173
  }
1133
1174
 
1134
1175
  function read(): TelegramWorkspaceAdmissionLedgerSnapshot {
1135
- const state = readState(options.path, options.profileKey);
1176
+ let state: TelegramWorkspaceAdmissionState;
1177
+ if (stateProfile !== undefined) {
1178
+ if (!isStorageCurrent()) authorityChanged("Telegram consolidated admission storage authority changed.");
1179
+ try {
1180
+ const file = readTelegramRuntimeState(path);
1181
+ const value = Object.hasOwn(file.profiles, stateProfile) ? file.profiles[stateProfile]?.admission : undefined;
1182
+ state = normalizeAdmissionState(value, profileKey, stateProfile);
1183
+ if (!isStorageCurrent()) authorityChanged("Telegram consolidated admission storage authority changed.");
1184
+ } catch (error) { return mapStateError(error); }
1185
+ } else state = readState(options.path, options.profileKey);
1136
1186
  return {
1137
1187
  profileKey: state.profileKey,
1138
1188
  leases: state.leases.map(cloneLease),
@@ -1675,8 +1725,8 @@ export function createTelegramWorkspaceAdmissionLedger(
1675
1725
  }
1676
1726
 
1677
1727
  return {
1678
- getProfileKey: () => options.profileKey,
1679
- getOwner: () => ({ ...options.owner }),
1728
+ getProfileKey: () => stateProfile === undefined ? options.profileKey : profileKey,
1729
+ getOwner: () => ({ ...(stateProfile === undefined ? options.owner : boundOwner) }),
1680
1730
  listReservedSlots: () => {
1681
1731
  const fence = read().fence;
1682
1732
  return fence && isTelegramWorkspaceRetirementFence(fence) ? [fence.slot] : [];
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Durable Workspace identity value construction
3
+ * Zones: workspace identity, session identity, key encoding
4
+ * Owns exact session/CWD normalization, bounded keys and local instance-slot encoding.
5
+ * Excludes allocation, bindings/targets, storage-reference approval, custody and effects.
6
+ */
7
+ import { createHash } from "node:crypto";
8
+ import { posix, resolve } from "node:path";
9
+
10
+ export interface TelegramWorkspaceBindingIdentity {
11
+ cwd: string;
12
+ workspaceKey: string;
13
+ /** Exact durable Pi session identity; absent only on legacy cwd-only bindings. */
14
+ sessionId?: string;
15
+ /** Full SHA-256 index component for session-qualified bindings. */
16
+ sessionKey?: string;
17
+ /** Immutable legacy binding-key component, not the displayed global letter. */
18
+ instanceSlot: string;
19
+ bindingKey: string;
20
+ /** Profile-wide letter reserved by the transient claim. */
21
+ slot?: string;
22
+ }
23
+
24
+ export const TELEGRAM_WORKSPACE_KEY_MAX_LENGTH = 180;
25
+ const TELEGRAM_SESSION_ID_MAX_LENGTH = 256;
26
+
27
+ export function normalizeTelegramSessionId(
28
+ sessionId: string,
29
+ ): string | undefined {
30
+ if (typeof sessionId !== "string") return undefined;
31
+ const normalized = sessionId.trim();
32
+ return normalized && Buffer.byteLength(normalized, "utf8") <=
33
+ TELEGRAM_SESSION_ID_MAX_LENGTH
34
+ ? normalized
35
+ : undefined;
36
+ }
37
+
38
+ export function createTelegramSessionKey(
39
+ sessionId: string,
40
+ ): string | undefined {
41
+ const normalized = normalizeTelegramSessionId(sessionId);
42
+ return normalized
43
+ ? createHash("sha256").update(normalized).digest("hex")
44
+ : undefined;
45
+ }
46
+
47
+ export function normalizeTelegramWorkspacePath(
48
+ cwd: string,
49
+ ): string | undefined {
50
+ const trimmed = cwd.trim();
51
+ if (!trimmed) return undefined;
52
+ const normalized = (trimmed.startsWith("/")
53
+ ? posix.normalize(trimmed)
54
+ : resolve(trimmed).replaceAll("\\", "/"));
55
+ const withoutTrailingSeparators =
56
+ normalized.length > 1 ? normalized.replace(/\/+$/u, "") : normalized;
57
+ return process.platform === "win32"
58
+ ? withoutTrailingSeparators.replace(/^([A-Z]):/u, (_, drive: string) =>
59
+ `${drive.toLowerCase()}:`,
60
+ )
61
+ : withoutTrailingSeparators;
62
+ }
63
+
64
+ export function createTelegramWorkspaceDirectoryKey(
65
+ cwd: string,
66
+ ): string | undefined {
67
+ const normalized = normalizeTelegramWorkspacePath(cwd);
68
+ if (!normalized) return undefined;
69
+ const readable =
70
+ normalized
71
+ .replace(/[^\p{L}\p{N}._-]+/gu, "-")
72
+ .replace(/^-+|-+$/gu, "") || "root";
73
+ const candidate = `--${readable}--`;
74
+ if (candidate.length <= TELEGRAM_WORKSPACE_KEY_MAX_LENGTH) return candidate;
75
+ const digest = createHash("sha256")
76
+ .update(normalized)
77
+ .digest("hex")
78
+ .slice(0, 12);
79
+ const prefixLength =
80
+ TELEGRAM_WORKSPACE_KEY_MAX_LENGTH - digest.length - 5;
81
+ return `--${readable.slice(0, prefixLength)}-${digest}--`;
82
+ }
83
+
84
+ function createTelegramWorkspaceInstanceSlot(
85
+ ordinal: number,
86
+ ): string | undefined {
87
+ if (!Number.isSafeInteger(ordinal) || ordinal < 0) return undefined;
88
+ let value = ordinal + 1;
89
+ let slot = "";
90
+ while (value > 0) {
91
+ value -= 1;
92
+ slot = String.fromCharCode(97 + (value % 26)) + slot;
93
+ value = Math.floor(value / 26);
94
+ }
95
+ return slot;
96
+ }
97
+
98
+ export function createTelegramWorkspaceBindingIdentityWithKey(
99
+ cwd: string,
100
+ workspaceKey: string,
101
+ ordinal: number,
102
+ sessionId?: string,
103
+ ): TelegramWorkspaceBindingIdentity | undefined {
104
+ const instanceSlot = createTelegramWorkspaceInstanceSlot(ordinal);
105
+ if (!instanceSlot) return undefined;
106
+ const normalizedSessionId = sessionId === undefined
107
+ ? undefined
108
+ : normalizeTelegramSessionId(sessionId);
109
+ const sessionKey = normalizedSessionId
110
+ ? createTelegramSessionKey(normalizedSessionId)
111
+ : undefined;
112
+ if (sessionId !== undefined && (!normalizedSessionId || !sessionKey)) {
113
+ return undefined;
114
+ }
115
+ const legacyBindingKey = instanceSlot === "a"
116
+ ? workspaceKey
117
+ : `${workspaceKey}${instanceSlot}`;
118
+ return {
119
+ cwd,
120
+ workspaceKey,
121
+ ...(normalizedSessionId && sessionKey
122
+ ? { sessionId: normalizedSessionId, sessionKey }
123
+ : {}),
124
+ instanceSlot,
125
+ bindingKey: sessionKey
126
+ ? `${legacyBindingKey}-s-${sessionKey}`
127
+ : legacyBindingKey,
128
+ };
129
+ }
130
+
131
+ export function createTelegramWorkspaceBindingIdentity(
132
+ cwd: string,
133
+ ordinal = 0,
134
+ sessionId?: string,
135
+ ): TelegramWorkspaceBindingIdentity | undefined {
136
+ const normalized = normalizeTelegramWorkspacePath(cwd);
137
+ const workspaceKey = normalized
138
+ ? createTelegramWorkspaceDirectoryKey(normalized)
139
+ : undefined;
140
+ if (!normalized || !workspaceKey) return undefined;
141
+ return createTelegramWorkspaceBindingIdentityWithKey(
142
+ normalized,
143
+ workspaceKey,
144
+ ordinal,
145
+ sessionId,
146
+ );
147
+ }