@llblab/pi-kit 0.27.5 → 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 (194) hide show
  1. package/BACKLOG.md +3 -1
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +4 -4
  4. package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
  5. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +6 -2
  6. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +4 -1
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +4 -3
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +26 -13
  9. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  10. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +3 -1
  11. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +2 -2
  12. package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -1
  13. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +4 -3
  14. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +25 -12
  15. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  16. package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
  17. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
  18. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  19. package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
  20. package/node_modules/@llblab/pi-telegram/README.md +15 -5
  21. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  22. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
  23. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
  24. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
  25. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
  26. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
  27. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
  28. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
  29. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
  30. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
  31. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
  32. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
  33. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
  34. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
  35. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
  36. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
  37. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
  38. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
  39. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
  40. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
  41. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
  42. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
  43. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
  44. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
  45. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
  46. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
  47. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
  48. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
  49. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
  50. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
  51. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
  52. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
  53. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
  54. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
  55. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
  56. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
  57. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
  58. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
  59. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
  60. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
  61. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
  62. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
  63. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
  64. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
  65. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
  66. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
  67. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
  68. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
  69. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
  70. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
  71. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
  72. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
  73. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
  74. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
  75. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
  76. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
  77. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
  78. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
  79. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
  80. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
  81. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
  82. package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
  83. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
  84. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
  85. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
  86. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
  87. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
  88. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
  89. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
  90. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
  91. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
  92. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
  93. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
  94. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
  95. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
  96. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
  97. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
  98. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
  99. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
  100. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
  101. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
  102. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
  103. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
  104. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
  105. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
  106. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
  107. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
  108. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
  109. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
  110. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
  111. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
  112. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
  113. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
  114. package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
  115. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
  116. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
  117. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
  118. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
  119. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
  120. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
  121. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
  122. package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
  123. package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
  124. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  125. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  126. package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
  127. package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
  128. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  129. package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
  130. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
  131. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  132. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
  133. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
  134. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  135. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  136. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
  137. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
  138. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
  139. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
  140. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
  141. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
  142. package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
  143. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
  144. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
  145. package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
  146. package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
  147. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
  148. package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
  149. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
  150. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
  151. package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
  152. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
  153. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
  154. package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
  155. package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
  156. package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
  157. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
  158. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
  159. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
  160. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
  161. package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
  162. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
  163. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
  164. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
  165. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
  166. package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
  167. package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
  168. package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
  169. package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
  170. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
  171. package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
  172. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
  173. package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
  174. package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
  175. package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
  176. package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
  177. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  178. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
  179. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
  180. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
  181. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
  182. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
  183. package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
  184. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
  185. package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
  186. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
  187. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
  188. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
  189. package/node_modules/@llblab/pi-telegram/package.json +6 -6
  190. package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
  191. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
  192. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
  193. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
  194. package/package.json +3 -3
@@ -1,113 +1,27 @@
1
1
  /**
2
2
  * Telegram thread binding helpers
3
- * Zones: multi-instance bus, Telegram UI threads, volatile extension state
4
- * Owns current live instance-binding to Telegram UI thread mappings backed by Bot API ForumTopic/message_thread_id transport
3
+ * Zones: multi-instance bus, Telegram UI threads, durable Workspace state
4
+ * Owns live mappings, Workspace bindings and guarded Restore transitions; routing and transport effects stay outside.
5
5
  */
6
6
  import { createHash, randomUUID } from "node:crypto";
7
- import { chmodSync, existsSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
8
- import { chmod, mkdir, readFile, rename, unlink, writeFile, } from "node:fs/promises";
9
- import { dirname, posix, resolve } from "node:path";
10
- import { isDeepStrictEqual } from "node:util";
7
+ import { chmodSync, closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFile as readFileCallback, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
8
+ import { chmod, mkdir, unlink, writeFile, } from "node:fs/promises";
9
+ import { basename, dirname, join } from "node:path";
10
+ import { isDeepStrictEqual, promisify } from "node:util";
11
11
  import { isTelegramApiCommitUnknownError, TelegramApiCommitUnknownError, } from "./telegram-api.js";
12
- import { withTelegramFileTransaction } from "./locks.js";
12
+ import { areTelegramTargetsEqual as targetMatches, getTelegramTargetKey as getTargetRecoveryHintKey, } from "./target.js";
13
+ import { isWireRecord as restoreObject, isNonEmptyWireString as restoreText } from "./wire.js";
14
+ import { chooseTelegramThreadName, getTelegramManualThreadDisplayNameValidationError, getTelegramThreadNameLeadingSlot, getTelegramTopicIdentityName, getTelegramTopicName as buildTelegramTopicName, getTelegramTopicTitleForThreadName, isTelegramTopicThreadNameValidForSlot, normalizeTelegramTopicTargetThreadName, } from "./thread-naming.js";
15
+ export { chooseTelegramThreadName, createTelegramThreadName, getTelegramManualThreadDisplayNameValidationError, getTelegramTopicIdentityName, getTelegramTopicThreadNameValidationError, getTelegramTopicTitleForThreadName, isTelegramTopicThreadNameValidForSlot, } from "./thread-naming.js";
16
+ // Preserve the legacy provision-request signature without a runtime wrapper.
17
+ export const getTelegramTopicName = buildTelegramTopicName;
18
+ import { TELEGRAM_WORKSPACE_KEY_MAX_LENGTH, createTelegramSessionKey, createTelegramWorkspaceBindingIdentityWithKey, createTelegramWorkspaceDirectoryKey, normalizeTelegramSessionId, normalizeTelegramWorkspacePath, } from "./workspace-identity.js";
19
+ export { createTelegramWorkspaceBindingIdentity, createTelegramWorkspaceDirectoryKey, normalizeTelegramSessionId, normalizeTelegramWorkspacePath, } from "./workspace-identity.js";
20
+ import { withTelegramFileTransaction, readTelegramRuntimeState, } from "./locks.js";
13
21
  import * as ThreadReconciler from "./thread-reconciler.js";
22
+ import { createTelegramRuntimeProjectionStore } from "./status.js";
14
23
  import { planTelegramWorkspaceSlotAllocation, TELEGRAM_WORKSPACE_SLOTS, TelegramWorkspaceSlotUnavailableError, } from "./workspace-slots.js";
15
24
  import { resolveAgentDir, resolveTelegramProfileTempFilePath, } from "./paths.js";
16
- const TELEGRAM_WORKSPACE_KEY_MAX_LENGTH = 180;
17
- const TELEGRAM_SESSION_ID_MAX_LENGTH = 256;
18
- export function normalizeTelegramSessionId(sessionId) {
19
- if (typeof sessionId !== "string")
20
- return undefined;
21
- const normalized = sessionId.trim();
22
- return normalized && Buffer.byteLength(normalized, "utf8") <=
23
- TELEGRAM_SESSION_ID_MAX_LENGTH
24
- ? normalized
25
- : undefined;
26
- }
27
- export function createTelegramSessionKey(sessionId) {
28
- const normalized = normalizeTelegramSessionId(sessionId);
29
- return normalized
30
- ? createHash("sha256").update(normalized).digest("hex")
31
- : undefined;
32
- }
33
- export function normalizeTelegramWorkspacePath(cwd) {
34
- const trimmed = cwd.trim();
35
- if (!trimmed)
36
- return undefined;
37
- const normalized = (trimmed.startsWith("/")
38
- ? posix.normalize(trimmed)
39
- : resolve(trimmed).replaceAll("\\", "/"));
40
- const withoutTrailingSeparators = normalized.length > 1 ? normalized.replace(/\/+$/u, "") : normalized;
41
- return process.platform === "win32"
42
- ? withoutTrailingSeparators.replace(/^([A-Z]):/u, (_, drive) => `${drive.toLowerCase()}:`)
43
- : withoutTrailingSeparators;
44
- }
45
- export function createTelegramWorkspaceDirectoryKey(cwd) {
46
- const normalized = normalizeTelegramWorkspacePath(cwd);
47
- if (!normalized)
48
- return undefined;
49
- const readable = normalized
50
- .replace(/[^\p{L}\p{N}._-]+/gu, "-")
51
- .replace(/^-+|-+$/gu, "") || "root";
52
- const candidate = `--${readable}--`;
53
- if (candidate.length <= TELEGRAM_WORKSPACE_KEY_MAX_LENGTH)
54
- return candidate;
55
- const digest = createHash("sha256")
56
- .update(normalized)
57
- .digest("hex")
58
- .slice(0, 12);
59
- const prefixLength = TELEGRAM_WORKSPACE_KEY_MAX_LENGTH - digest.length - 5;
60
- return `--${readable.slice(0, prefixLength)}-${digest}--`;
61
- }
62
- function createTelegramWorkspaceInstanceSlot(ordinal) {
63
- if (!Number.isSafeInteger(ordinal) || ordinal < 0)
64
- return undefined;
65
- let value = ordinal + 1;
66
- let slot = "";
67
- while (value > 0) {
68
- value -= 1;
69
- slot = String.fromCharCode(97 + (value % 26)) + slot;
70
- value = Math.floor(value / 26);
71
- }
72
- return slot;
73
- }
74
- function createTelegramWorkspaceBindingIdentityWithKey(cwd, workspaceKey, ordinal, sessionId) {
75
- const instanceSlot = createTelegramWorkspaceInstanceSlot(ordinal);
76
- if (!instanceSlot)
77
- return undefined;
78
- const normalizedSessionId = sessionId === undefined
79
- ? undefined
80
- : normalizeTelegramSessionId(sessionId);
81
- const sessionKey = normalizedSessionId
82
- ? createTelegramSessionKey(normalizedSessionId)
83
- : undefined;
84
- if (sessionId !== undefined && (!normalizedSessionId || !sessionKey)) {
85
- return undefined;
86
- }
87
- const legacyBindingKey = instanceSlot === "a"
88
- ? workspaceKey
89
- : `${workspaceKey}${instanceSlot}`;
90
- return {
91
- cwd,
92
- workspaceKey,
93
- ...(normalizedSessionId && sessionKey
94
- ? { sessionId: normalizedSessionId, sessionKey }
95
- : {}),
96
- instanceSlot,
97
- bindingKey: sessionKey
98
- ? `${legacyBindingKey}-s-${sessionKey}`
99
- : legacyBindingKey,
100
- };
101
- }
102
- export function createTelegramWorkspaceBindingIdentity(cwd, ordinal = 0, sessionId) {
103
- const normalized = normalizeTelegramWorkspacePath(cwd);
104
- const workspaceKey = normalized
105
- ? createTelegramWorkspaceDirectoryKey(normalized)
106
- : undefined;
107
- if (!normalized || !workspaceKey)
108
- return undefined;
109
- return createTelegramWorkspaceBindingIdentityWithKey(normalized, workspaceKey, ordinal, sessionId);
110
- }
111
25
  const TELEGRAM_THREAD_RESERVATION_TTL_MS = 15 * 60 * 1000;
112
26
  function getNextMonotonicSlot(records, reservations, pendingProvisions, nowMs, lastSlot) {
113
27
  let cursorCode;
@@ -188,6 +102,16 @@ export function createTelegramCleanupTargetProtection(store, departingRecord) {
188
102
  // optional fields; neither changes the authority represented by a snapshot.
189
103
  const sameSnapshot = (left, right) => isDeepStrictEqual(JSON.parse(JSON.stringify(left)), JSON.parse(JSON.stringify(right)));
190
104
  return (target, action) => {
105
+ // A source-bound temporary tab is removed only by its own Forward/Cancel, never by generic reconciliation.
106
+ if (store.listTemporaryThreadTargets) {
107
+ try {
108
+ if (store.listTemporaryThreadTargets().some(candidate => targetMatches(candidate, target)))
109
+ return true;
110
+ }
111
+ catch {
112
+ return true;
113
+ }
114
+ }
191
115
  for (const record of store.list()) {
192
116
  if (!targetMatches(record.target, target))
193
117
  continue;
@@ -261,41 +185,6 @@ export function commitTelegramWorkspaceProvisionBinding(input) {
261
185
  input.store.removePendingProvision(provision.id);
262
186
  return displayTitle === undefined ? committed : { ...committed, displayTitle };
263
187
  }
264
- function hashString(value) {
265
- let hash = 2166136261;
266
- for (let index = 0; index < value.length; index += 1) {
267
- hash ^= value.charCodeAt(index);
268
- hash = Math.imul(hash, 16777619);
269
- }
270
- return hash >>> 0;
271
- }
272
- function getWorkspaceHint(cwd) {
273
- if (!cwd)
274
- return undefined;
275
- const parts = cwd.split("/").filter(Boolean);
276
- const last = parts.at(-1)?.trim();
277
- if (!last)
278
- return undefined;
279
- return (last
280
- .replace(/[^\p{L}\p{N}._-]+/gu, " ")
281
- .trim()
282
- .slice(0, 32) || undefined);
283
- }
284
- export function createTelegramThreadName(input) {
285
- const workspace = getWorkspaceHint(input.cwd);
286
- const roleMark = input.role === "leader"
287
- ? "Leader"
288
- : input.role === "follower"
289
- ? "Follower"
290
- : undefined;
291
- const slot = input.slot ? `Thread ${input.slot}` : undefined;
292
- const peerSalt = input.peers?.slice().sort().join("|") ?? "";
293
- const fallback = `Instance ${hashString(`${input.seed}|${input.cwd ?? ""}|${input.role ?? ""}|${peerSalt}|${input.slot ?? ""}`)
294
- .toString(36)
295
- .slice(0, 4)}`;
296
- return ([slot, workspace, roleMark].filter(Boolean).join(" ").slice(0, 96) ||
297
- fallback);
298
- }
299
188
  export function getTelegramStatePath(agentDir = resolveAgentDir(), profileName) {
300
189
  return resolveTelegramProfileTempFilePath("state", "json", agentDir, profileName);
301
190
  }
@@ -326,7 +215,7 @@ export function setTelegramLeaderSessionHandoff(handoff) {
326
215
  else
327
216
  store[TELEGRAM_LEADER_SESSION_HANDOFF_KEY] = handoff;
328
217
  }
329
- export function isTelegramLeaderSessionHandoffFresh(handoff, options = {}) {
218
+ function isTelegramLeaderSessionHandoffFresh(handoff, options = {}) {
330
219
  if (!handoff)
331
220
  return false;
332
221
  const pid = options.pid ?? process.pid;
@@ -476,6 +365,7 @@ function normalizeRecord(value) {
476
365
  if (typeof record.createdAtMs !== "number" ||
477
366
  typeof record.updatedAtMs !== "number")
478
367
  return undefined;
368
+ const threadName = getPersistedThreadName(record);
479
369
  const normalized = {
480
370
  profileKey: getTelegramThreadOwnerKey(owner),
481
371
  owner,
@@ -483,7 +373,7 @@ function normalizeRecord(value) {
483
373
  status,
484
374
  createdAtMs: record.createdAtMs,
485
375
  updatedAtMs: record.updatedAtMs,
486
- threadName: getPersistedThreadName(record),
376
+ ...(threadName !== undefined ? { threadName } : {}),
487
377
  ...(typeof record.manualThreadName === "string" &&
488
378
  normalizeTelegramTopicTargetThreadName(record.manualThreadName)
489
379
  ? { manualThreadName: normalizeTelegramTopicTargetThreadName(record.manualThreadName) }
@@ -557,6 +447,23 @@ function cloneIdentityRecord(identity) {
557
447
  function getWorkspaceBindingMapKey(binding) {
558
448
  return `${binding.cwd}\u0000${binding.sessionId ?? ""}\u0000${binding.instanceSlot}`;
559
449
  }
450
+ function normalizeWorkspaceJournalSources(value) {
451
+ if (!Array.isArray(value) || value.length > 256)
452
+ return undefined;
453
+ const sources = new Map();
454
+ for (const entry of value) {
455
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
456
+ return undefined;
457
+ const source = entry;
458
+ if (Object.keys(source).some(key => key !== "sessionId" && key !== "recipientBindingKey") ||
459
+ typeof source.sessionId !== "string" || normalizeTelegramSessionId(source.sessionId) !== source.sessionId ||
460
+ typeof source.recipientBindingKey !== "string" || !source.recipientBindingKey || source.recipientBindingKey.length > 512)
461
+ return undefined;
462
+ const normalized = { sessionId: source.sessionId, recipientBindingKey: source.recipientBindingKey };
463
+ sources.set(JSON.stringify(normalized), normalized);
464
+ }
465
+ return Array.from(sources.values());
466
+ }
560
467
  function normalizeWorkspaceBindingRecord(value) {
561
468
  if (!value || typeof value !== "object" || Array.isArray(value))
562
469
  return undefined;
@@ -617,6 +524,9 @@ function normalizeWorkspaceBindingRecord(value) {
617
524
  record.journalBindingKeys.every((key) => typeof key === "string" && key.length > 0 && key.length <= 512)
618
525
  ? Array.from(new Set(record.journalBindingKeys))
619
526
  : undefined;
527
+ const journalSources = record.journalSources === undefined ? undefined : normalizeWorkspaceJournalSources(record.journalSources);
528
+ if (record.journalSources !== undefined && !journalSources)
529
+ return undefined;
620
530
  return {
621
531
  cwd,
622
532
  workspaceKey: record.workspaceKey,
@@ -632,7 +542,8 @@ function normalizeWorkspaceBindingRecord(value) {
632
542
  ? { inactiveSinceMs: record.inactiveSinceMs }
633
543
  : {}),
634
544
  ...(journalBindingKeys ? { journalBindingKeys } : {}),
635
- ...(record.journalBindingsComplete === true && journalBindingKeys
545
+ ...(journalSources ? { journalSources } : {}),
546
+ ...(record.journalBindingsComplete === true && (journalBindingKeys || journalSources)
636
547
  ? { journalBindingsComplete: true }
637
548
  : {}),
638
549
  target: { chatId: target.chatId, threadId: target.threadId },
@@ -643,7 +554,9 @@ function normalizeWorkspaceBindingRecord(value) {
643
554
  };
644
555
  }
645
556
  function cloneWorkspaceBinding(binding) {
646
- return { ...binding, target: { ...binding.target } };
557
+ return { ...binding, target: { ...binding.target },
558
+ ...(binding.journalBindingKeys ? { journalBindingKeys: [...binding.journalBindingKeys] } : {}),
559
+ ...(binding.journalSources ? { journalSources: binding.journalSources.map(source => ({ ...source })) } : {}) };
647
560
  }
648
561
  function cloneSessionReplacementIntent(intent) {
649
562
  return { ...intent, target: { ...intent.target } };
@@ -909,6 +822,351 @@ function normalizeReservation(value) {
909
822
  : undefined,
910
823
  };
911
824
  }
825
+ function normalizeTelegramWorkspaceRelocationRequest(value) {
826
+ if (!value || typeof value !== "object" || Array.isArray(value))
827
+ return undefined;
828
+ const request = value;
829
+ const binding = normalizeWorkspaceBindingRecord(request.binding);
830
+ const owner = normalizeRecord(request.owner);
831
+ const target = request.target;
832
+ if (typeof request.operationId !== "string" || !request.operationId ||
833
+ !binding?.sessionId || !binding.slot || !/^[A-Z]$/.test(binding.slot) || binding.inactiveSinceMs !== undefined ||
834
+ !owner || owner.status !== "active" || !owner.instanceId || owner.slot !== binding.slot ||
835
+ !["leader", "manual-follower"].includes(owner.owner?.kind ?? "") ||
836
+ (owner.owner?.kind === "leader" &&
837
+ (owner.owner.cwd !== binding.cwd || owner.owner.instanceId !== owner.instanceId)) ||
838
+ !targetMatches(binding.target, owner.target) ||
839
+ !Number.isSafeInteger(binding.target.chatId) || binding.target.chatId === 0 ||
840
+ !Number.isSafeInteger(binding.target.threadId) || binding.target.threadId <= 0 ||
841
+ !target || target.chatId !== binding.target.chatId ||
842
+ !Number.isSafeInteger(target.threadId) || target.threadId <= 0 ||
843
+ targetMatches(target, binding.target))
844
+ return undefined;
845
+ const normalized = { operationId: request.operationId, binding, owner,
846
+ target: { chatId: target.chatId, threadId: target.threadId } };
847
+ return isDeepStrictEqual(value, normalized) ? normalized : undefined;
848
+ }
849
+ const restoreInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
850
+ const restoreKeys = (value, allowed) => Object.keys(value).every(key => allowed.includes(key));
851
+ const isTelegramWorkspaceRestoreExecutor = (value) => restoreObject(value) &&
852
+ restoreKeys(value, ["instanceId", "leaderEpoch"]) && restoreText(value.instanceId) && restoreText(value.leaderEpoch);
853
+ export const isTelegramWorkspaceRestoreRecipient = (value) => restoreObject(value) &&
854
+ restoreKeys(value, ["kind", "instanceId", "sessionId", "generation"]) &&
855
+ (value.kind === "leader" || value.kind === "follower") && restoreText(value.instanceId) && restoreText(value.sessionId) && restoreText(value.generation);
856
+ export function isTelegramWorkspaceRestoreRequest(value) {
857
+ if (!restoreObject(value))
858
+ return false;
859
+ const { source, ...relocation } = value;
860
+ return !!normalizeTelegramWorkspaceRelocationRequest(relocation) && restoreObject(source) &&
861
+ restoreKeys(source, ["journalBindingKey", "updateIds"]) && restoreText(source.journalBindingKey) &&
862
+ Array.isArray(source.updateIds) && source.updateIds.length > 0 &&
863
+ source.updateIds.every((id, index, ids) => restoreInteger(id) && (index === 0 || id > ids[index - 1]));
864
+ }
865
+ function isTelegramWorkspaceRestoreSettlement(value, source) {
866
+ if (!restoreObject(value) || value.journalBindingKey !== source.journalBindingKey || !Array.isArray(value.updateIds) ||
867
+ value.updateIds.length === 0 || !value.updateIds.every((id, index, ids) => restoreInteger(id) && source.updateIds.includes(id) &&
868
+ (index === 0 || id > ids[index - 1])))
869
+ return false;
870
+ return value.kind === "completed" ? restoreKeys(value, ["journalBindingKey", "updateIds", "kind"]) :
871
+ (value.kind === "queued" || value.kind === "queue-completed") && restoreKeys(value, ["journalBindingKey", "updateIds", "kind", "receiptId", "queueKind"]) &&
872
+ restoreText(value.receiptId) && (value.queueKind === "prompt" || value.queueKind === "control");
873
+ }
874
+ function isWorkspaceRestoreSourceAcceptance(value, operation) {
875
+ if (!restoreObject(value) || value.journalBindingKey !== operation.request.source.journalBindingKey || !restoreInteger(value.updateId) ||
876
+ !operation.request.source.updateIds.includes(value.updateId) || typeof value.sourceSha256 !== "string" ||
877
+ !/^[a-f0-9]{64}$/u.test(value.sourceSha256) || !isTelegramWorkspaceRestoreRecipient(value.recipient) ||
878
+ value.recipient.sessionId !== operation.request.binding.sessionId || value.recipient.kind !== operation.recipient?.kind)
879
+ return false;
880
+ const keys = ["journalBindingKey", "updateId", "sourceSha256", "recipient", "kind"];
881
+ if (value.kind === "completed")
882
+ return value.recipient.kind === "leader" && restoreKeys(value, keys);
883
+ if (value.kind === "forwarded")
884
+ return value.recipient.kind === "follower" &&
885
+ restoreKeys(value, [...keys, "deliveryId", "recipientBindingKey"]) && typeof value.deliveryId === "string" &&
886
+ /^telegram-follower-v1-[a-f0-9]{64}$/u.test(value.deliveryId) && restoreText(value.recipientBindingKey);
887
+ return value.kind === "queued" && value.recipient.kind === "leader" &&
888
+ restoreKeys(value, [...keys, "receiptId", "queueKind", "queueOwnerSha256"]) && restoreText(value.receiptId) &&
889
+ (value.queueKind === "prompt" || value.queueKind === "control") && typeof value.queueOwnerSha256 === "string" &&
890
+ /^[a-f0-9]{64}$/u.test(value.queueOwnerSha256);
891
+ }
892
+ /** Stable acceptance scope, not a removal ACK; adoption and mutable progress cannot change it. */
893
+ export function getTelegramWorkspaceRestoreSourceCompletionSha256(operation, acceptance) {
894
+ if (!isTelegramWorkspaceRestoreRequest(operation.request) || operation.operatorUserId !== operation.request.target.chatId ||
895
+ !isWorkspaceRestoreSourceAcceptance(acceptance, operation) ||
896
+ !operation.routing?.acceptances?.some(value => isDeepStrictEqual(value, acceptance))) {
897
+ throw new Error("Workspace Restore retained acceptance scope is unavailable.");
898
+ }
899
+ const frame = { kind: "workspace-restore-source-completion-v1", request: operation.request,
900
+ operatorUserId: operation.operatorUserId, acceptance };
901
+ return createHash("sha256").update(JSON.stringify(frame, function (_key, value) {
902
+ return restoreObject(value) ? Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]])) : value;
903
+ })).digest("hex");
904
+ }
905
+ function workspaceRestoreAcceptancesConsistent(acceptances) {
906
+ return acceptances.every((value, index) => (index === 0 || value.updateId > acceptances[index - 1].updateId) &&
907
+ acceptances.slice(0, index).every(previous => {
908
+ if (value.kind === "forwarded" && previous.kind === "forwarded")
909
+ return value.deliveryId !== previous.deliveryId;
910
+ if (value.kind !== "queued" || previous.kind !== "queued" || value.receiptId !== previous.receiptId)
911
+ return true;
912
+ return value.queueKind === previous.queueKind && value.queueOwnerSha256 === previous.queueOwnerSha256 &&
913
+ isDeepStrictEqual(value.recipient, previous.recipient);
914
+ }));
915
+ }
916
+ function workspaceRestoreSettlementMatchesAcceptance(evidence, acceptances) {
917
+ return evidence.updateIds.every(id => {
918
+ const accepted = acceptances.find(value => value.updateId === id);
919
+ if (!accepted)
920
+ return evidence.kind !== "queue-completed"; // Legacy admission is retained, never terminal receipt proof.
921
+ return evidence.kind === "completed" ? accepted.kind !== "queued" : accepted.kind === "queued" &&
922
+ evidence.receiptId === accepted.receiptId && evidence.queueKind === accepted.queueKind;
923
+ });
924
+ }
925
+ function workspaceRestoreSourcesSettled(operation) {
926
+ const settled = new Set(operation.routing?.settlements.filter(value => value.kind !== "queued").flatMap(value => value.updateIds));
927
+ return operation.request.source.updateIds.every(id => settled.has(id));
928
+ }
929
+ function isWorkspaceRestoreIntent(value) {
930
+ if (!restoreObject(value) || !restoreKeys(value, ["request", "operatorUserId", "executor", "revision", "createdAtMs",
931
+ "updatedAtMs", "phase", "committedAtMs", "recipient", "readyRecipient", "routing"]) || !isTelegramWorkspaceRestoreRequest(value.request) ||
932
+ !restoreInteger(value.operatorUserId) || value.operatorUserId !== value.request.target.chatId ||
933
+ !isTelegramWorkspaceRestoreExecutor(value.executor) || !restoreInteger(value.revision) || !restoreInteger(value.createdAtMs) ||
934
+ !restoreInteger(value.updatedAtMs) || value.updatedAtMs < value.createdAtMs ||
935
+ !restoreInteger(value.committedAtMs))
936
+ return false;
937
+ const source = value.request.source;
938
+ if (value.routing !== undefined) {
939
+ if (value.phase !== "ready" || !restoreObject(value.routing) || !restoreKeys(value.routing, ["acceptances", "settlements", "cleanup"]) ||
940
+ !Array.isArray(value.routing.settlements) || !value.routing.settlements.every(item => isTelegramWorkspaceRestoreSettlement(item, source)))
941
+ return false;
942
+ if (value.routing.acceptances !== undefined) {
943
+ const acceptances = value.routing.acceptances;
944
+ if (!Array.isArray(acceptances) || !acceptances.length ||
945
+ !acceptances.every(item => isWorkspaceRestoreSourceAcceptance(item, value)) ||
946
+ !workspaceRestoreAcceptancesConsistent(acceptances))
947
+ return false;
948
+ }
949
+ const acceptances = (value.routing.acceptances ?? []);
950
+ if (!value.routing.settlements.every(item => workspaceRestoreSettlementMatchesAcceptance(item, acceptances)))
951
+ return false;
952
+ const ids = value.routing.settlements.flatMap(item => item.updateIds);
953
+ if (new Set(ids).size !== ids.length || (value.routing.cleanup !== undefined &&
954
+ (!["issued", "completed", "not-issued"].includes(value.routing.cleanup) ||
955
+ !workspaceRestoreSourcesSettled(value))))
956
+ return false;
957
+ }
958
+ if (value.readyRecipient !== undefined && (value.phase !== "ready" || !isTelegramWorkspaceRestoreRecipient(value.readyRecipient) ||
959
+ value.readyRecipient.sessionId !== value.request.binding.sessionId))
960
+ return false;
961
+ if (value.phase === "relocated")
962
+ return value.recipient === undefined;
963
+ return (value.phase === "recipient-issued" || value.phase === "ready") && isTelegramWorkspaceRestoreRecipient(value.recipient) &&
964
+ value.recipient.sessionId === value.request.binding.sessionId;
965
+ }
966
+ function isTemporaryThreadInput(value) {
967
+ return restoreObject(value) && restoreKeys(value, ["journalBindingKey", "updateIds"]) && restoreText(value.journalBindingKey) &&
968
+ Array.isArray(value.updateIds) && value.updateIds.length > 0 && value.updateIds.length <= TEMPORARY_THREAD_INPUT_CAPACITY &&
969
+ value.updateIds.every((id, index, ids) => restoreInteger(id) && (index === 0 || id > ids[index - 1]));
970
+ }
971
+ export function getTelegramTemporaryThreadInputs(entry) {
972
+ return structuredClone(entry.inputs ?? [{ journalBindingKey: entry.source.journalBindingKey, updateIds: [entry.source.updateId] }]);
973
+ }
974
+ /** Every known group is durably cancelled or Forward-completed; still not deletion authority by itself. */
975
+ export function isTelegramTemporaryThreadFullyResolved(entry) {
976
+ const inputs = getTelegramTemporaryThreadInputs(entry), resolved = [...(entry.cancelledInputs ?? []), ...(entry.completedInputs ?? [])];
977
+ return resolved.length > 0 && inputs.every(input => resolved.some(other => isDeepStrictEqual(other, input)));
978
+ }
979
+ /** One unresolved group may release protection after completion when every other group has a durable terminal fact. */
980
+ function isTemporaryThreadReleasedByCompletion(entry, completed) {
981
+ if (!completed || !isTemporaryThreadInput(completed))
982
+ return false;
983
+ const inputs = getTelegramTemporaryThreadInputs(entry), cancelled = [...(entry.cancelledInputs ?? []), ...(entry.completedInputs ?? [])];
984
+ const same = (left, right) => isDeepStrictEqual(left, right);
985
+ return inputs.some(input => same(input, completed)) && !cancelled.some(input => same(input, completed)) &&
986
+ inputs.every(input => same(input, completed) || cancelled.some(other => same(other, input)));
987
+ }
988
+ function isTemporaryThreadEntry(value) {
989
+ if (!restoreObject(value) || !restoreKeys(value, ["source", "inputs", "cancelledInputs", "completedInputs", "forwardedInputs", "forwardProtocol", "cleanupIssued", "operatorUserId", "executor", "token", "phase", "target", "revision",
990
+ "createdAtMs", "updatedAtMs"]) || !restoreObject(value.source) || !restoreKeys(value.source, ["journalBindingKey", "updateId"]) ||
991
+ !restoreText(value.source.journalBindingKey) || !restoreInteger(value.source.updateId) ||
992
+ !restoreInteger(value.operatorUserId) || value.operatorUserId === 0 || !isTelegramWorkspaceRestoreExecutor(value.executor) ||
993
+ typeof value.token !== "string" || !/^[a-f0-9]{32}$/u.test(value.token) || !restoreInteger(value.revision) ||
994
+ !restoreInteger(value.createdAtMs) || !restoreInteger(value.updatedAtMs) || value.updatedAtMs < value.createdAtMs)
995
+ return false;
996
+ const source = value.source;
997
+ if (value.forwardProtocol !== undefined && (value.forwardProtocol !== "one-shot-v1" || value.inputs === undefined))
998
+ return false;
999
+ if (value.inputs !== undefined) {
1000
+ const inputs = value.inputs;
1001
+ if (!Array.isArray(inputs) || inputs.length === 0 || inputs.length > TEMPORARY_THREAD_INPUT_CAPACITY ||
1002
+ !inputs.every(isTemporaryThreadInput) || !isDeepStrictEqual(inputs[0], { journalBindingKey: source.journalBindingKey, updateIds: [source.updateId] }) ||
1003
+ inputs.some((input, index) => input.journalBindingKey !== source.journalBindingKey ||
1004
+ inputs.slice(index + 1).some(other => other.updateIds.some(id => input.updateIds.includes(id)))))
1005
+ return false;
1006
+ }
1007
+ if (value.cancelledInputs !== undefined) {
1008
+ const cancelled = value.cancelledInputs;
1009
+ const inputs = Array.isArray(value.inputs) ? value.inputs : [{ journalBindingKey: source.journalBindingKey, updateIds: [source.updateId] }];
1010
+ if (!Array.isArray(cancelled) || cancelled.length === 0 || cancelled.length > TEMPORARY_THREAD_INPUT_CAPACITY ||
1011
+ !cancelled.every(isTemporaryThreadInput) || cancelled.some((input, index) => !inputs.some(candidate => isDeepStrictEqual(candidate, input)) || cancelled.slice(index + 1).some(other => isDeepStrictEqual(other, input))))
1012
+ return false;
1013
+ }
1014
+ if (value.completedInputs !== undefined) {
1015
+ const completed = value.completedInputs;
1016
+ const inputs = Array.isArray(value.inputs) ? value.inputs : [{ journalBindingKey: source.journalBindingKey, updateIds: [source.updateId] }];
1017
+ const cancelled = Array.isArray(value.cancelledInputs) ? value.cancelledInputs : [];
1018
+ if (!Array.isArray(completed) || completed.length === 0 || completed.length > TEMPORARY_THREAD_INPUT_CAPACITY ||
1019
+ !completed.every(isTemporaryThreadInput) || completed.some((input, index) => !inputs.some(candidate => isDeepStrictEqual(candidate, input)) || cancelled.some(other => isDeepStrictEqual(other, input)) ||
1020
+ completed.slice(index + 1).some(other => isDeepStrictEqual(other, input))))
1021
+ return false;
1022
+ }
1023
+ if (value.forwardedInputs !== undefined) {
1024
+ const forwarded = value.forwardedInputs;
1025
+ const inputs = Array.isArray(value.inputs) ? value.inputs : [{ journalBindingKey: source.journalBindingKey, updateIds: [source.updateId] }];
1026
+ const cancelled = Array.isArray(value.cancelledInputs) ? value.cancelledInputs : [];
1027
+ if (!Array.isArray(forwarded) || forwarded.length === 0 || forwarded.length > TEMPORARY_THREAD_INPUT_CAPACITY ||
1028
+ !forwarded.every(isTemporaryThreadInput) || forwarded.some((input, index) => !inputs.some(candidate => isDeepStrictEqual(candidate, input)) || cancelled.some(other => isDeepStrictEqual(other, input)) ||
1029
+ forwarded.slice(index + 1).some(other => isDeepStrictEqual(other, input))))
1030
+ return false;
1031
+ }
1032
+ if (value.cleanupIssued !== undefined && (value.cleanupIssued !== true ||
1033
+ !isTelegramTemporaryThreadFullyResolved(value)))
1034
+ return false;
1035
+ if (value.phase === "creating")
1036
+ return value.target === undefined && value.cancelledInputs === undefined && value.completedInputs === undefined && value.forwardedInputs === undefined && value.cleanupIssued === undefined &&
1037
+ (!Array.isArray(value.inputs) || value.inputs.length === 1);
1038
+ const target = value.target;
1039
+ return value.phase === "created" && restoreObject(target) && restoreKeys(target, ["chatId", "threadId"]) &&
1040
+ target.chatId === value.operatorUserId && Number.isSafeInteger(target.threadId) && target.threadId > 0;
1041
+ }
1042
+ /** Only an exact recorded source group may Restore into its temporary tab. */
1043
+ function isTemporaryThreadRestore(entry, operations) {
1044
+ return operations.some(({ request }) => !!entry.target && targetMatches(request.target, entry.target) &&
1045
+ ![...(entry.cancelledInputs ?? []), ...(entry.completedInputs ?? []), ...(entry.forwardedInputs ?? [])].some(input => input.journalBindingKey === request.source.journalBindingKey &&
1046
+ input.updateIds.some(id => request.source.updateIds.includes(id))) &&
1047
+ (entry.inputs ? entry.inputs.some(input => isDeepStrictEqual(input, request.source)) :
1048
+ request.source.journalBindingKey === entry.source.journalBindingKey && request.source.updateIds.includes(entry.source.updateId)));
1049
+ }
1050
+ function conflictsWithTemporaryThread(snapshot, target) {
1051
+ return !!target && !!snapshot?.temporaryThreads?.some(entry => !!entry.target && targetMatches(entry.target, target));
1052
+ }
1053
+ function conflictsWithWorkspaceRestoreProvision(request, provision) {
1054
+ if ("owner" in provision && !provision.workspaceBindingKey &&
1055
+ (provision.profileKey === request.owner.profileKey || provision.instanceId === request.owner.instanceId))
1056
+ return true;
1057
+ return ("workspaceBindingKey" in provision && provision.workspaceBindingKey === request.binding.bindingKey) || provision.slot === request.binding.slot ||
1058
+ !!provision.target && (targetMatches(provision.target, request.binding.target) || targetMatches(provision.target, request.target));
1059
+ }
1060
+ function assertWorkspaceRestoreBindingProtection(file) {
1061
+ for (const { request } of file.workspaceRestore?.operations ?? []) {
1062
+ if (file.pendingProvisions?.some(value => conflictsWithWorkspaceRestoreProvision(request, value)) ||
1063
+ file.reservations?.some(value => conflictsWithWorkspaceRestoreProvision(request, value))) {
1064
+ throw new Error("Protected Workspace Restore provisioning conflict.");
1065
+ }
1066
+ const bindings = file.workspaceBindings ?? [];
1067
+ const retained = bindings.filter(value => value.bindingKey === request.binding.bindingKey);
1068
+ const current = retained[0];
1069
+ const identity = ["bindingKey", "cwd", "workspaceKey", "instanceSlot", "sessionId", "sessionKey", "slot"];
1070
+ if (retained.length !== 1 || !current || identity.some(key => current[key] !== request.binding[key]) ||
1071
+ !targetMatches(current.target, request.target) || bindings.some(value => value !== current &&
1072
+ (value.slot === current.slot || targetMatches(value.target, request.binding.target) || targetMatches(value.target, request.target)))) {
1073
+ throw new Error("Protected Workspace Restore binding changed.");
1074
+ }
1075
+ const owners = (file.threads ?? []).filter(value => value.slot === current.slot ||
1076
+ targetMatches(value.target, request.binding.target) || targetMatches(value.target, request.target));
1077
+ if (owners.length > 1 || owners.some(value => value.slot !== current.slot || !targetMatches(value.target, request.target))) {
1078
+ throw new Error("Protected Workspace Restore owner target changed.");
1079
+ }
1080
+ }
1081
+ const operations = file.workspaceRestore?.operations ?? [];
1082
+ for (const entry of file.workspaceRestore?.temporaryThreads ?? []) {
1083
+ const target = entry.target;
1084
+ if (!target)
1085
+ continue;
1086
+ const claimed = [...(file.workspaceBindings ?? []), ...(file.threads ?? [])].some(value => targetMatches(value.target, target));
1087
+ if (file.pendingProvisions?.some(value => !!value.target && targetMatches(value.target, target)) ||
1088
+ file.reservations?.some(value => targetMatches(value.target, target)) ||
1089
+ (claimed && !isTemporaryThreadRestore(entry, operations))) {
1090
+ throw new Error("Protected temporary Thread target conflict.");
1091
+ }
1092
+ }
1093
+ }
1094
+ function workspaceRestoresConflict(left, right) {
1095
+ const targets = [left.binding.target, left.target];
1096
+ return left.operationId === right.operationId || left.binding.bindingKey === right.binding.bindingKey ||
1097
+ left.binding.slot === right.binding.slot || targets.some(target => targetMatches(target, right.binding.target) || targetMatches(target, right.target)) ||
1098
+ (left.source.journalBindingKey === right.source.journalBindingKey && right.source.updateIds.some(id => left.source.updateIds.includes(id)));
1099
+ }
1100
+ const WORKSPACE_RESTORE_MAX_BYTES = 1024 * 1024;
1101
+ const TEMPORARY_THREAD_CAPACITY = 26;
1102
+ const TEMPORARY_THREAD_INPUT_CAPACITY = 100;
1103
+ const WORKSPACE_SNAPSHOT_MAX_BYTES = 8 * WORKSPACE_RESTORE_MAX_BYTES;
1104
+ function parseWorkspaceRestore(value) {
1105
+ if (value === undefined)
1106
+ return undefined;
1107
+ const temporary = restoreObject(value) ? value.temporaryThreads : undefined;
1108
+ if (!restoreObject(value) || !restoreKeys(value, ["version", "profileName", "tokenSha256", "revision", "operations", "temporaryThreads"]) ||
1109
+ value.version !== 1 || !restoreText(value.profileName) || typeof value.tokenSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(value.tokenSha256) ||
1110
+ !restoreInteger(value.revision) || !Array.isArray(value.operations) || value.operations.length > 26 ||
1111
+ (temporary !== undefined && (!Array.isArray(temporary) || temporary.length === 0 ||
1112
+ temporary.length > TEMPORARY_THREAD_CAPACITY || !temporary.every(isTemporaryThreadEntry))) ||
1113
+ ((value.operations.length > 0 || temporary !== undefined) && value.revision === 0) || !value.operations.every(isWorkspaceRestoreIntent) ||
1114
+ Buffer.byteLength(JSON.stringify(value)) > WORKSPACE_RESTORE_MAX_BYTES)
1115
+ throw new Error("Invalid Workspace Restore evidence.");
1116
+ const snapshot = value;
1117
+ if (snapshot.operations.some((operation, index) => snapshot.operations.slice(index + 1).some(other => workspaceRestoresConflict(operation.request, other.request))))
1118
+ throw new Error("Conflicting Workspace Restore evidence.");
1119
+ const entries = snapshot.temporaryThreads ?? [];
1120
+ if (entries.some((entry, index) => entries.slice(index + 1).some(other => other.token === entry.token ||
1121
+ getTelegramTemporaryThreadInputs(entry).some(input => getTelegramTemporaryThreadInputs(other).some(candidate => input.journalBindingKey === candidate.journalBindingKey && input.updateIds.some(id => candidate.updateIds.includes(id)))) ||
1122
+ (!!entry.target && !!other.target && targetMatches(entry.target, other.target)))) ||
1123
+ entries.some(entry => !!entry.target && snapshot.operations.some(({ request }) => targetMatches(request.binding.target, entry.target) ||
1124
+ (targetMatches(request.target, entry.target) && !isTemporaryThreadRestore(entry, [{ request }])))))
1125
+ throw new Error("Conflicting Workspace Restore evidence.");
1126
+ return snapshot;
1127
+ }
1128
+ /** All Restore reads and publishers share these physical-file checks; there is no repairing read. */
1129
+ const readWorkspaceDescriptor = promisify(readFileCallback);
1130
+ function readWorkspaceSnapshot(path, requirePrivate = true, asynchronous = false) {
1131
+ let before;
1132
+ try {
1133
+ before = lstatSync(path, { bigint: true });
1134
+ }
1135
+ catch (error) {
1136
+ if (error.code === "ENOENT")
1137
+ return undefined;
1138
+ throw error;
1139
+ }
1140
+ const uid = process.getuid?.();
1141
+ const privateFile = uid === undefined || (before.uid === BigInt(uid) && (before.mode & 63n) === 0n);
1142
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n || before.size > BigInt(WORKSPACE_SNAPSHOT_MAX_BYTES) ||
1143
+ (requirePrivate && !privateFile))
1144
+ throw new Error("Workspace snapshot must be a bounded private regular file.");
1145
+ const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
1146
+ let closeImmediately = true;
1147
+ try {
1148
+ const opened = fstatSync(fd, { bigint: true });
1149
+ if (opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== before.size || opened.mtimeNs !== before.mtimeNs)
1150
+ throw new Error("Workspace snapshot changed during inspection.");
1151
+ const decode = (content) => {
1152
+ const value = JSON.parse(content);
1153
+ // Legacy metadata may predate private storage; a Restore-bearing snapshot never may.
1154
+ if (!privateFile && restoreObject(value) && value.workspaceRestore !== undefined)
1155
+ throw new Error("Workspace snapshot must be a bounded private regular file.");
1156
+ return value;
1157
+ };
1158
+ if (asynchronous) {
1159
+ const reading = readWorkspaceDescriptor(fd, "utf8").then(decode);
1160
+ closeImmediately = false;
1161
+ return reading.finally(() => closeSync(fd));
1162
+ }
1163
+ return decode(readFileSync(fd, "utf8"));
1164
+ }
1165
+ finally {
1166
+ if (closeImmediately)
1167
+ closeSync(fd);
1168
+ }
1169
+ }
912
1170
  function parseTopicTargetFile(value) {
913
1171
  if (!value || typeof value !== "object" || Array.isArray(value)) {
914
1172
  return {
@@ -920,6 +1178,13 @@ function parseTopicTargetFile(value) {
920
1178
  };
921
1179
  }
922
1180
  const file = value;
1181
+ // The unpublished two-file draft is not migration or source-reconstruction authority.
1182
+ if ((file.workspaceRelocations !== undefined && (!Array.isArray(file.workspaceRelocations) || file.workspaceRelocations.length !== 0)) ||
1183
+ (file.workspaceRelocationRevision !== undefined && file.workspaceRelocationRevision !== 0))
1184
+ throw new Error("Unmigrated Workspace relocation evidence.");
1185
+ const workspaceRestore = parseWorkspaceRestore(file.workspaceRestore);
1186
+ if (file.version !== 1 && workspaceRestore)
1187
+ throw new Error("Unsupported Workspace Restore snapshot.");
923
1188
  if (file.version !== 1) {
924
1189
  return {
925
1190
  version: 1,
@@ -948,6 +1213,8 @@ function parseTopicTargetFile(value) {
948
1213
  workspaceBindings: Array.isArray(file.workspaceBindings)
949
1214
  ? file.workspaceBindings.flatMap((binding) => {
950
1215
  const normalized = normalizeWorkspaceBindingRecord(binding);
1216
+ if (!normalized && binding && typeof binding === "object" && "journalSources" in binding)
1217
+ throw new Error("Invalid Workspace session journal evidence.");
951
1218
  return normalized ? [normalized] : [];
952
1219
  })
953
1220
  : [],
@@ -957,6 +1224,7 @@ function parseTopicTargetFile(value) {
957
1224
  return normalized ? [normalized] : [];
958
1225
  })
959
1226
  : [],
1227
+ ...(workspaceRestore ? { workspaceRestore } : {}),
960
1228
  sessionReplacement: normalizeTelegramSessionReplacementIntent(file.sessionReplacement),
961
1229
  reservations: Array.isArray(file.reservations)
962
1230
  ? file.reservations.flatMap((reservation) => {
@@ -984,18 +1252,91 @@ function parseTopicTargetFile(value) {
984
1252
  : [],
985
1253
  };
986
1254
  }
1255
+ /** The unified section accepts only lossless current-format Workspace evidence, never tolerant legacy repair. */
1256
+ export function parseTelegramWorkspaceStateSection(value, profile) {
1257
+ if (value === undefined)
1258
+ return undefined;
1259
+ if (!restoreObject(value) || value.version !== 1 || value.source !== "snapshot" ||
1260
+ !Number.isSafeInteger(value.writtenAtMs) || value.writtenAtMs < 0 ||
1261
+ !restoreObject(value.bot) || !Array.isArray(value.threads))
1262
+ throw new Error("Invalid consolidated Workspace section.");
1263
+ if (Buffer.byteLength(JSON.stringify(value)) > WORKSPACE_SNAPSHOT_MAX_BYTES)
1264
+ throw new Error("Workspace snapshot byte capacity reached.");
1265
+ const parsed = parseTopicTargetFile(value), rawThreads = value.threads;
1266
+ const wire = JSON.parse(JSON.stringify(parsed));
1267
+ wire.threads = wire.threads.map((record, index) => {
1268
+ const copied = { ...record };
1269
+ const original = rawThreads[index];
1270
+ if (!restoreObject(original))
1271
+ throw new Error("Invalid consolidated Workspace thread evidence.");
1272
+ if (!Object.hasOwn(original, "profileKey"))
1273
+ delete copied.profileKey;
1274
+ return copied;
1275
+ });
1276
+ for (const key of Object.keys(value)) {
1277
+ if (!Object.hasOwn(wire, key) || !isDeepStrictEqual(value[key], wire[key]))
1278
+ throw new Error("Consolidated Workspace evidence cannot drop or normalize unknown facts.");
1279
+ }
1280
+ if ((parsed.workspaceRestore && parsed.workspaceRestore.profileName !== profile) ||
1281
+ (parsed.sessionReplacement && parsed.sessionReplacement.profileName !== profile) ||
1282
+ parsed.threads.some(record => record.owner && "telegramProfile" in record.owner &&
1283
+ record.owner.telegramProfile !== undefined && record.owner.telegramProfile !== profile))
1284
+ throw new Error("Foreign consolidated Workspace profile evidence.");
1285
+ assertWorkspaceRestoreBindingProtection(parsed);
1286
+ return parsed;
1287
+ }
1288
+ export function resolveTelegramWorkspaceProvisionRecoveryPath(statePath, profileName = "default", layout) {
1289
+ if (layout !== undefined && layout !== "consolidated")
1290
+ throw new Error("Invalid Workspace provisioning recovery layout.");
1291
+ return layout === "consolidated"
1292
+ ? join(dirname(statePath), "runtime", `${basename(statePath)}.provision-recovery.${createHash("sha256").update(profileName).digest("hex").slice(0, 16)}.json`)
1293
+ : `${statePath}.provision-recovery.json`;
1294
+ }
1295
+ /** Prepared Workspace IO adapter; transition/CAS policy remains with the existing Threads owner. */
1296
+ export function createTelegramConsolidatedWorkspaceStorage(options) {
1297
+ const { getPath, getProfile, captureAuthority, publishIfOwned } = options;
1298
+ const profileName = () => getProfile() ?? "default";
1299
+ const readRaw = () => {
1300
+ const path = getPath(), profile = profileName(), root = readTelegramRuntimeState(path);
1301
+ const raw = Object.hasOwn(root.profiles, profile) ? root.profiles[profile]?.workspace : undefined;
1302
+ parseTelegramWorkspaceStateSection(raw, profile);
1303
+ if (path !== getPath() || profile !== profileName())
1304
+ throw new Error("Workspace storage scope changed during observation.");
1305
+ return raw;
1306
+ };
1307
+ return {
1308
+ readRaw,
1309
+ read: () => parseTelegramWorkspaceStateSection(readRaw(), profileName()),
1310
+ capturePublication() {
1311
+ const path = getPath(), profile = profileName(), authority = captureAuthority();
1312
+ if (!authority)
1313
+ return undefined;
1314
+ const isCurrent = () => path === getPath() && profile === profileName() && authority() === true;
1315
+ if (!isCurrent())
1316
+ return undefined;
1317
+ return (mutate, publication) => {
1318
+ const sourceCurrent = publication?.isCurrent;
1319
+ const currentGrant = () => isCurrent() && (sourceCurrent?.() ?? true);
1320
+ if (!currentGrant())
1321
+ return { committed: false };
1322
+ return publishIfOwned("workspace", current => {
1323
+ parseTelegramWorkspaceStateSection(current, profile);
1324
+ const next = mutate(current);
1325
+ if (next.value === undefined && current !== undefined)
1326
+ throw new Error("Consolidated Workspace section removal is not authorized.");
1327
+ parseTelegramWorkspaceStateSection(next.value, profile);
1328
+ return next;
1329
+ }, { ...publication, isCurrent: currentGrant, expectedScope: { path, profile } });
1330
+ };
1331
+ },
1332
+ };
1333
+ }
987
1334
  function getTelegramStateSemanticSnapshot(value) {
988
1335
  if (!value || typeof value !== "object" || Array.isArray(value))
989
1336
  return undefined;
990
1337
  const { writtenAtMs: _writtenAtMs, ...semantic } = value;
991
1338
  return semantic;
992
1339
  }
993
- function targetMatches(left, right) {
994
- return left.chatId === right.chatId && left.threadId === right.threadId;
995
- }
996
- function getTargetRecoveryHintKey(target) {
997
- return `${target.chatId}:${target.threadId ?? "private"}`;
998
- }
999
1340
  function parseFollowerRecoveryHints(value) {
1000
1341
  const hints = new Map();
1001
1342
  if (!value || typeof value !== "object" || Array.isArray(value))
@@ -1059,7 +1400,32 @@ function isPendingProvisionLiveOrTargeted(provision, nowMs) {
1059
1400
  }
1060
1401
  return !!provision.target;
1061
1402
  }
1403
+ /** Resolving selects a scope-bound view of the existing Workspace owner; it creates no files. */
1404
+ export function createTelegramWorkspaceRestoreResolver(deps) {
1405
+ const { getProfileName, getBotToken, threadStore, agentDir } = deps;
1406
+ let active;
1407
+ return () => {
1408
+ const profile = getProfileName();
1409
+ const profileName = profile ?? "default";
1410
+ const token = getBotToken();
1411
+ if (!token)
1412
+ return undefined;
1413
+ const tokenSha256 = createHash("sha256").update(token).digest("hex");
1414
+ if (active?.profileName === profileName && active.tokenSha256 === tokenSha256)
1415
+ return active.store;
1416
+ const store = threadStore.workspaceRestore({ profileName, tokenSha256,
1417
+ legacyPath: resolveTelegramProfileTempFilePath("workspace-restore", "json", agentDir, profile),
1418
+ isCurrentScope: () => (getProfileName() ?? "default") === profileName &&
1419
+ createHash("sha256").update(getBotToken() ?? "").digest("hex") === tokenSha256 });
1420
+ active = { profileName, tokenSha256, store };
1421
+ return store;
1422
+ };
1423
+ }
1062
1424
  export function createTelegramTopicTargetStore(options) {
1425
+ if (options.consolidated && options.commitPersist)
1426
+ throw new Error("Workspace must select one publication backend.");
1427
+ const consolidated = options.consolidated ? { ...options.consolidated } : undefined;
1428
+ const commitPersist = options.commitPersist?.bind(options);
1063
1429
  const getNowMs = options.getNowMs ?? Date.now;
1064
1430
  const captureExternalReservedSlots = () => {
1065
1431
  try {
@@ -1079,6 +1445,7 @@ export function createTelegramTopicTargetStore(options) {
1079
1445
  let identities = new Map();
1080
1446
  let workspaceBindings = new Map();
1081
1447
  let workspaceRetirements = [];
1448
+ let workspaceRestore;
1082
1449
  let sessionReplacement;
1083
1450
  let workspaceRetirementCommitInFlight = false;
1084
1451
  const hasWorkspaceRetirementConflict = (input) => workspaceRetirements.some((intent) => (input.bindingKey !== undefined && intent.binding.bindingKey === input.bindingKey) ||
@@ -1093,10 +1460,14 @@ export function createTelegramTopicTargetStore(options) {
1093
1460
  let followerRecoveryHints = new Map();
1094
1461
  let loaded = false;
1095
1462
  let loadedPath;
1463
+ let loadedProfile;
1464
+ let observedWorkspaceSemantic;
1096
1465
  let dirty = false;
1097
1466
  let mutationRevision = 0;
1098
- let statusRevision = 0;
1099
1467
  let persistQueue = Promise.resolve();
1468
+ let statusQueue = Promise.resolve();
1469
+ let lastStatusSemantic;
1470
+ let lastStatusPath;
1100
1471
  let statusSnapshot = {};
1101
1472
  const reconcileWorkspaceSuffixExposure = () => {
1102
1473
  const directoryCounts = new Map();
@@ -1168,6 +1539,11 @@ export function createTelegramTopicTargetStore(options) {
1168
1539
  .sort((left, right) => right.updatedAtMs - left.updatedAtMs)[0];
1169
1540
  };
1170
1541
  const getPath = () => typeof options.path === "function" ? options.path() : options.path;
1542
+ /** The non-canonical projection sits beside canonical state (`state*.json` → `status*.json`). */
1543
+ const getStatusPath = () => {
1544
+ const statePath = getPath(), name = basename(statePath);
1545
+ return join(dirname(statePath), /^state/u.test(name) ? name.replace(/^state/u, "status") : `${name}.status`);
1546
+ };
1171
1547
  const getTelegramProfile = () => typeof options.telegramProfile === "function"
1172
1548
  ? options.telegramProfile()
1173
1549
  : options.telegramProfile;
@@ -1181,9 +1557,72 @@ export function createTelegramTopicTargetStore(options) {
1181
1557
  }
1182
1558
  return { ...owner, telegramProfile };
1183
1559
  };
1184
- const getRecoveryPath = (path) => `${path}.provision-recovery.json`;
1185
- const readProvisionRecoveries = (path) => {
1560
+ const activeProfile = () => getTelegramProfile() ?? "default";
1561
+ const scopeMatches = (path, profile) => getPath() === path && activeProfile() === profile;
1562
+ const loadedScopeMatches = (path) => loadedPath === path && (!consolidated || loadedProfile === activeProfile());
1563
+ const workspaceIO = consolidated ? createTelegramConsolidatedWorkspaceStorage({ getPath, getProfile: getTelegramProfile, ...consolidated }) : undefined;
1564
+ const runtimeProjection = consolidated ? createTelegramRuntimeProjectionStore({ getPath, getProfile: getTelegramProfile,
1565
+ captureAuthority: consolidated.captureAuthority, getNowMs,
1566
+ storage: {
1567
+ read({ path, profile }) { const root = readTelegramRuntimeState(path); return Object.hasOwn(root.profiles, profile) ? root.profiles[profile]?.runtime : undefined; },
1568
+ publish(expectedScope, mutate, isCurrent) {
1569
+ const outcome = consolidated.publishIfOwned("runtime", current => {
1570
+ const next = mutate(current);
1571
+ return { value: next.value, result: next.changed };
1572
+ }, { isCurrent, expectedScope });
1573
+ return outcome.committed && outcome.result;
1574
+ },
1575
+ },
1576
+ }) : undefined;
1577
+ let workspaceFrame;
1578
+ const readStoreSnapshot = (path, requirePrivate = true, asynchronous = false) => {
1579
+ if (!workspaceIO)
1580
+ return readWorkspaceSnapshot(path, requirePrivate, asynchronous);
1581
+ if (path !== getPath())
1582
+ throw new Error("Workspace storage scope changed during observation.");
1583
+ if (workspaceFrame) {
1584
+ if (!scopeMatches(workspaceFrame.path, workspaceFrame.profile))
1585
+ throw new Error("Workspace transaction scope changed.");
1586
+ return workspaceFrame.value;
1587
+ }
1588
+ const value = workspaceIO.readRaw();
1589
+ return asynchronous ? Promise.resolve(value) : value;
1590
+ };
1591
+ const withStoreTransaction = (path, operation, publication) => {
1592
+ if (!workspaceIO)
1593
+ return withTelegramFileTransaction(`${path}.transaction`, operation);
1594
+ if (workspaceFrame)
1595
+ throw new Error("Nested Workspace transaction is not allowed.");
1596
+ const profile = activeProfile(), publish = workspaceIO.capturePublication();
1597
+ if (!publish || path !== getPath())
1598
+ throw new Error("Workspace publication authority changed.");
1599
+ const outcome = publish(value => {
1600
+ workspaceFrame = { path, profile, value };
1601
+ try {
1602
+ const result = operation();
1603
+ return { value: workspaceFrame.value, result };
1604
+ }
1605
+ finally {
1606
+ workspaceFrame = undefined;
1607
+ }
1608
+ }, publication);
1609
+ if (!outcome.committed)
1610
+ throw new Error("Workspace publication authority changed.");
1611
+ return outcome.result;
1612
+ };
1613
+ const getRecoveryPath = (path) => consolidated
1614
+ ? resolveTelegramWorkspaceProvisionRecoveryPath(path, activeProfile(), "consolidated")
1615
+ : resolveTelegramWorkspaceProvisionRecoveryPath(path);
1616
+ const readProvisionRecoveries = (path, strict = false) => {
1186
1617
  const recoveryPath = getRecoveryPath(path);
1618
+ if (strict) {
1619
+ const value = readWorkspaceSnapshot(recoveryPath);
1620
+ if (value === undefined)
1621
+ return {};
1622
+ if (!restoreObject(value))
1623
+ throw new Error("Invalid Workspace provisioning recovery evidence.");
1624
+ return value;
1625
+ }
1187
1626
  if (!existsSync(recoveryPath))
1188
1627
  return {};
1189
1628
  try {
@@ -1196,14 +1635,43 @@ export function createTelegramTopicTargetStore(options) {
1196
1635
  return {};
1197
1636
  }
1198
1637
  };
1638
+ const assertRestoreRecoveryProtection = (path, operations, provisions, requireKnownTargets = false, recoveryEvidence) => {
1639
+ if (!operations.length)
1640
+ return;
1641
+ const recoveries = recoveryEvidence ?? readProvisionRecoveries(path, true);
1642
+ for (const provision of provisions) {
1643
+ let target = provision.target;
1644
+ if (Object.hasOwn(recoveries, provision.id)) {
1645
+ const recovery = recoveries[provision.id];
1646
+ if (!restoreObject(recovery))
1647
+ throw new Error("Invalid Workspace provisioning recovery evidence.");
1648
+ if (recovery.instanceId === provision.instanceId && recovery.profileKey === provision.profileKey &&
1649
+ recovery.leaderEpoch === provision.leaderEpoch) {
1650
+ const observed = recovery.target;
1651
+ if (!restoreObject(observed) || typeof observed.chatId !== "number" || !Number.isSafeInteger(observed.chatId) ||
1652
+ !restoreInteger(observed.threadId) || observed.threadId === 0)
1653
+ throw new Error("Invalid Workspace provisioning recovery evidence.");
1654
+ const recovered = { chatId: observed.chatId, threadId: observed.threadId };
1655
+ if (target && !targetMatches(target, recovered))
1656
+ throw new Error("Conflicting Workspace provisioning target evidence.");
1657
+ target = recovered;
1658
+ }
1659
+ }
1660
+ if (requireKnownTargets && !target)
1661
+ throw new Error("Workspace Restore target availability is unknown during unfinished creation.");
1662
+ if (operations.some(({ request }) => conflictsWithWorkspaceRestoreProvision(request, { ...provision, target })))
1663
+ throw new Error("Protected Workspace Restore provisioning recovery conflict.");
1664
+ }
1665
+ };
1199
1666
  const resetForPath = (path) => {
1200
- if (loadedPath === path)
1667
+ if (loadedScopeMatches(path))
1201
1668
  return;
1202
1669
  botState = { threadMode: "unknown" };
1203
1670
  records = new Map();
1204
1671
  identities = new Map();
1205
1672
  workspaceBindings = new Map();
1206
1673
  workspaceRetirements = [];
1674
+ workspaceRestore = undefined;
1207
1675
  sessionReplacement = undefined;
1208
1676
  workspaceRetirementCommitInFlight = false;
1209
1677
  workspaceClaims = new Map();
@@ -1216,16 +1684,62 @@ export function createTelegramTopicTargetStore(options) {
1216
1684
  loaded = false;
1217
1685
  dirty = false;
1218
1686
  loadedPath = path;
1687
+ loadedProfile = activeProfile();
1688
+ observedWorkspaceSemantic = undefined;
1219
1689
  };
1220
- const loadFromDisk = async () => {
1690
+ const observeWorkspaceRestore = (incoming) => {
1691
+ if (workspaceRestore && (!incoming || incoming.profileName !== workspaceRestore.profileName ||
1692
+ incoming.tokenSha256 !== workspaceRestore.tokenSha256 || incoming.revision < workspaceRestore.revision ||
1693
+ (incoming.revision === workspaceRestore.revision && !isDeepStrictEqual(incoming, workspaceRestore))))
1694
+ throw new Error("Workspace Restore evidence moved backwards or changed without revision.");
1695
+ };
1696
+ const withRestoreRegistration = (candidate, publish) => {
1221
1697
  const path = getPath();
1698
+ withStoreTransaction(path, () => {
1699
+ const file = parseTopicTargetFile(readStoreSnapshot(path, false));
1700
+ observeWorkspaceRestore(file.workspaceRestore);
1701
+ const operations = file.workspaceRestore?.operations ?? [];
1702
+ if (operations.length) {
1703
+ if ((options.canPersist && !options.canPersist()) || getPath() !== path)
1704
+ throw new Error("Workspace Restore registration authority changed.");
1705
+ assertWorkspaceRestoreBindingProtection(file);
1706
+ assertRestoreRecoveryProtection(path, operations, file.pendingProvisions ?? []);
1707
+ for (const { request } of operations) {
1708
+ if (targetMatches(candidate.target, request.binding.target) ||
1709
+ ((candidate.bindingKey === request.binding.bindingKey || candidate.slot === request.binding.slot ||
1710
+ targetMatches(candidate.target, request.target)) &&
1711
+ (candidate.bindingKey !== request.binding.bindingKey || candidate.slot !== request.binding.slot ||
1712
+ !targetMatches(candidate.target, request.target))))
1713
+ throw new Error("Protected Workspace Restore registration conflicts with its binding or target.");
1714
+ }
1715
+ }
1716
+ if (publish && candidate.bindingKey !== undefined) {
1717
+ const bindings = (file.workspaceBindings ?? []).filter(binding => binding.bindingKey === candidate.bindingKey);
1718
+ if (bindings.length !== 1 || bindings[0]?.slot !== candidate.slot || !targetMatches(bindings[0].target, candidate.target))
1719
+ throw new Error("Workspace registration binding changed before publication.");
1720
+ }
1721
+ publish?.();
1722
+ });
1723
+ };
1724
+ const loadFromDisk = async () => {
1725
+ const path = getPath(), profile = activeProfile();
1222
1726
  resetForPath(path);
1223
- if (!existsSync(path)) {
1727
+ const revision = mutationRevision;
1728
+ const rawFile = await readStoreSnapshot(path, !!workspaceRestore, true);
1729
+ // A read begun before a local mutation cannot replace the newly admitted projection.
1730
+ if (mutationRevision !== revision || getPath() !== path || (consolidated && !scopeMatches(path, profile)))
1731
+ return;
1732
+ if (consolidated)
1733
+ observedWorkspaceSemantic = getTelegramStateSemanticSnapshot(rawFile);
1734
+ if (rawFile === undefined) {
1735
+ if (workspaceRestore?.revision)
1736
+ throw new Error("Workspace Restore evidence disappeared.");
1224
1737
  botState = { threadMode: "unknown" };
1225
1738
  records = new Map();
1226
1739
  identities = new Map();
1227
1740
  workspaceBindings = new Map();
1228
1741
  workspaceRetirements = [];
1742
+ workspaceRestore = undefined;
1229
1743
  sessionReplacement = undefined;
1230
1744
  workspaceRetirementCommitInFlight = false;
1231
1745
  reservations = [];
@@ -1236,15 +1750,18 @@ export function createTelegramTopicTargetStore(options) {
1236
1750
  loaded = true;
1237
1751
  return;
1238
1752
  }
1239
- const revision = mutationRevision;
1240
- const content = await readFile(path, "utf8");
1241
- // A read begun before a local mutation must not replace the newly admitted
1242
- // binding/cleanup state with its older disk snapshot.
1243
- if (mutationRevision !== revision || getPath() !== path)
1244
- return;
1245
- const rawFile = JSON.parse(content);
1246
1753
  const file = parseTopicTargetFile(rawFile);
1247
- followerRecoveryHints = parseFollowerRecoveryHints(rawFile);
1754
+ observeWorkspaceRestore(file.workspaceRestore);
1755
+ const recoveries = readProvisionRecoveries(path, !!file.workspaceRestore?.operations.length);
1756
+ assertRestoreRecoveryProtection(path, file.workspaceRestore?.operations ?? [], file.pendingProvisions ?? [], false, recoveries);
1757
+ try {
1758
+ followerRecoveryHints = parseFollowerRecoveryHints(consolidated
1759
+ ? runtimeProjection?.read()
1760
+ : JSON.parse(readFileSync(getStatusPath(), "utf8")));
1761
+ }
1762
+ catch {
1763
+ followerRecoveryHints = new Map();
1764
+ }
1248
1765
  botState = file.bot;
1249
1766
  const scopedRecords = file.threads.map((record) => cloneRecord({
1250
1767
  ...record,
@@ -1264,6 +1781,7 @@ export function createTelegramTopicTargetStore(options) {
1264
1781
  cloneWorkspaceBinding(binding),
1265
1782
  ]));
1266
1783
  workspaceRetirements = (file.workspaceRetirements ?? []).map(cloneWorkspaceRetirementIntent);
1784
+ workspaceRestore = structuredClone(file.workspaceRestore);
1267
1785
  sessionReplacement = file.sessionReplacement
1268
1786
  ? cloneSessionReplacementIntent(file.sessionReplacement)
1269
1787
  : undefined;
@@ -1275,9 +1793,7 @@ export function createTelegramTopicTargetStore(options) {
1275
1793
  .filter((reservation) => reservation.expiresAtMs === undefined ||
1276
1794
  reservation.expiresAtMs > nowMs)
1277
1795
  .map((reservation) => ({ ...reservation }));
1278
- const recoveries = readProvisionRecoveries(path);
1279
1796
  pendingProvisions = (file.pendingProvisions ?? [])
1280
- .filter((provision) => isPendingProvisionLiveOrTargeted(provision, nowMs))
1281
1797
  .map((provision) => {
1282
1798
  const recovery = recoveries[provision.id];
1283
1799
  const recoveryMatches = recovery?.instanceId === provision.instanceId &&
@@ -1292,7 +1808,8 @@ export function createTelegramTopicTargetStore(options) {
1292
1808
  ? { target: { ...provision.target } }
1293
1809
  : {}),
1294
1810
  };
1295
- });
1811
+ })
1812
+ .filter((provision) => isPendingProvisionLiveOrTargeted(provision, nowMs));
1296
1813
  pendingCleanups = (file.pendingCleanups ?? []).map((intent) => ({
1297
1814
  ...intent,
1298
1815
  target: { ...intent.target },
@@ -1305,6 +1822,12 @@ export function createTelegramTopicTargetStore(options) {
1305
1822
  dirty = false;
1306
1823
  };
1307
1824
  const markDirty = () => {
1825
+ if (consolidated && !loadedScopeMatches(getPath())) {
1826
+ if (loadedPath !== undefined)
1827
+ throw new Error("Workspace projection must refresh after scope change.");
1828
+ loadedPath = getPath();
1829
+ loadedProfile = activeProfile();
1830
+ }
1308
1831
  loaded = true;
1309
1832
  dirty = true;
1310
1833
  mutationRevision += 1;
@@ -1321,10 +1844,14 @@ export function createTelegramTopicTargetStore(options) {
1321
1844
  }
1322
1845
  return false;
1323
1846
  };
1324
- const persistSnapshot = (transition) => {
1847
+ const persistSnapshot = (transition, isExactJournalFrame) => {
1848
+ const submittedPath = getPath(), submittedProfile = activeProfile();
1849
+ const publishWorkspace = workspaceIO?.capturePublication();
1325
1850
  const persist = persistQueue.then(async () => {
1326
1851
  const path = getPath();
1327
- if (loadedPath !== path && !dirty)
1852
+ if (workspaceIO && (!publishWorkspace || !scopeMatches(submittedPath, submittedProfile) || (dirty && !loadedScopeMatches(path))))
1853
+ return false;
1854
+ if (!loadedScopeMatches(path) && !dirty)
1328
1855
  resetForPath(path);
1329
1856
  if (options.canPersist && !options.canPersist()) {
1330
1857
  if (!transition)
@@ -1333,6 +1860,8 @@ export function createTelegramTopicTargetStore(options) {
1333
1860
  }
1334
1861
  if (!dirty || !loaded)
1335
1862
  await loadFromDisk();
1863
+ if (workspaceIO && (!scopeMatches(submittedPath, submittedProfile) || !loadedScopeMatches(path)))
1864
+ return false;
1336
1865
  if (transition && !transition.isCurrent())
1337
1866
  return false;
1338
1867
  const nowMs = getNowMs();
@@ -1347,21 +1876,71 @@ export function createTelegramTopicTargetStore(options) {
1347
1876
  if (owners.length !== 1 || !isDeepStrictEqual(normalizeRecord(owners[0]), transition.owner))
1348
1877
  return false;
1349
1878
  }
1879
+ if (transition?.kind === "relocate") {
1880
+ const { binding, owner, nextTarget } = transition;
1881
+ const slots = captureExternalReservedSlots();
1882
+ const sourceBindings = Array.from(workspaceBindings.values())
1883
+ .filter((value) => targetMatches(value.target, binding.target));
1884
+ const sourceOwners = currentRecords.filter((value) => targetMatches(value.target, binding.target));
1885
+ if ((workspaceRestore?.operations ?? []).some((operation) => workspaceRestoresConflict(operation.request, transition.restore.operations.at(-1).request)) ||
1886
+ !binding.sessionId || !binding.slot || !/^[A-Z]$/.test(binding.slot) ||
1887
+ binding.inactiveSinceMs !== undefined || owner.status !== "active" ||
1888
+ !owner.instanceId || owner.slot !== binding.slot ||
1889
+ !["leader", "manual-follower"].includes(owner.owner?.kind ?? "") ||
1890
+ (owner.owner?.kind === "leader" &&
1891
+ (owner.owner.cwd !== binding.cwd || owner.owner.instanceId !== owner.instanceId)) ||
1892
+ !targetMatches(owner.target, binding.target) ||
1893
+ sourceBindings.length !== 1 || !isDeepStrictEqual(sourceBindings[0], binding) ||
1894
+ sourceOwners.length !== 1 || !isDeepStrictEqual(sourceOwners[0], owner) ||
1895
+ !slots || slots.includes(binding.slot) || workspaceRetirementCommitInFlight ||
1896
+ hasWorkspaceRetirementConflict(binding) || hasWorkspaceRetirementConflict({ target: nextTarget }) ||
1897
+ Array.from(workspaceBindings.values()).some((value) => value.bindingKey !== binding.bindingKey &&
1898
+ (value.slot === binding.slot || targetMatches(value.target, nextTarget))) ||
1899
+ currentRecords.some((value) => targetMatches(value.target, nextTarget) ||
1900
+ (!targetMatches(value.target, binding.target) && value.slot === binding.slot)) ||
1901
+ Array.from(workspaceClaims.values()).some((value) => value.identity.bindingKey === binding.bindingKey || value.identity.slot === binding.slot) ||
1902
+ reservations.some((value) => value.slot === binding.slot ||
1903
+ targetMatches(value.target, binding.target) || targetMatches(value.target, nextTarget)) ||
1904
+ pendingProvisions.some((value) => value.workspaceBindingKey === binding.bindingKey ||
1905
+ value.instanceId === owner.instanceId || value.profileKey === owner.profileKey ||
1906
+ value.slot === binding.slot || (value.target &&
1907
+ (targetMatches(value.target, binding.target) || targetMatches(value.target, nextTarget)))) ||
1908
+ pendingCleanups.some((value) => targetMatches(value.target, binding.target) ||
1909
+ targetMatches(value.target, nextTarget)) ||
1910
+ syncObservations.some((value) => targetMatches(value.target, nextTarget) &&
1911
+ (value.syncStatus === "deleted" || value.syncStatus === "closed")) ||
1912
+ (sessionReplacement && ((sessionReplacement.cwd === binding.cwd &&
1913
+ sessionReplacement.sourceSessionId === binding.sessionId) ||
1914
+ targetMatches(sessionReplacement.target, binding.target) ||
1915
+ targetMatches(sessionReplacement.target, nextTarget))))
1916
+ return false;
1917
+ }
1350
1918
  records = new Map(currentRecords.map((record) => [
1351
1919
  getRecordOwnerKey(record),
1352
1920
  cloneRecord(record),
1353
1921
  ]));
1354
1922
  const persistedRevision = mutationRevision;
1355
- const persistedStatusRevision = statusRevision;
1923
+ const expectedRestore = structuredClone(workspaceRestore);
1924
+ const assertRestoreEvidenceCurrent = (raw, fromTransaction = false) => {
1925
+ if (isExactJournalFrame?.() === false)
1926
+ throw new Error("Telegram Workspace journal evidence publication lost its exact frame or authority.");
1927
+ const disk = parseTopicTargetFile(fromTransaction ? raw : readStoreSnapshot(path, transition?.kind === "relocate" || !!expectedRestore));
1928
+ if (!isDeepStrictEqual(disk.workspaceRestore, expectedRestore))
1929
+ throw new Error("Workspace Restore evidence changed before publication.");
1930
+ assertWorkspaceRestoreBindingProtection(disk);
1931
+ assertRestoreRecoveryProtection(path, [...(disk.workspaceRestore?.operations ?? []), ...(file.workspaceRestore?.operations ?? [])], [...(disk.pendingProvisions ?? []), ...(file.pendingProvisions ?? [])]);
1932
+ const additions = (file.workspaceRestore?.operations ?? []).filter(operation => !disk.workspaceRestore?.operations.some(retained => retained.request.operationId === operation.request.operationId));
1933
+ assertRestoreRecoveryProtection(path, additions, disk.pendingProvisions ?? [], true);
1934
+ };
1356
1935
  const file = {
1357
1936
  version: 1,
1358
1937
  source: "snapshot",
1359
1938
  writtenAtMs: nowMs,
1360
1939
  bot: botState,
1361
- ...statusSnapshot,
1362
1940
  identities: Array.from(identities.values()).map(cloneIdentityRecord),
1363
1941
  workspaceBindings: Array.from(workspaceBindings.values()).map(cloneWorkspaceBinding),
1364
1942
  workspaceRetirements: workspaceRetirements.map(cloneWorkspaceRetirementIntent),
1943
+ ...(workspaceRestore ? { workspaceRestore: structuredClone(workspaceRestore) } : {}),
1365
1944
  ...(sessionReplacement
1366
1945
  ? { sessionReplacement: cloneSessionReplacementIntent(sessionReplacement) }
1367
1946
  : {}),
@@ -1393,60 +1972,153 @@ export function createTelegramTopicTargetStore(options) {
1393
1972
  bindings[0].slot !== record.slot)
1394
1973
  return false;
1395
1974
  }
1396
- file.threads = file.threads.filter((candidate) => candidate !== record);
1397
- for (const binding of file.workspaceBindings) {
1398
- if (targetMatches(binding.target, transition.target) && binding.inactiveSinceMs === undefined) {
1399
- binding.inactiveSinceMs = nowMs;
1975
+ if (transition.kind === "relocate") {
1976
+ if (transition.restore.revision !== (workspaceRestore?.revision ?? 0) + 1 ||
1977
+ !isDeepStrictEqual(transition.restore.operations.slice(0, -1), workspaceRestore?.operations ?? []))
1978
+ return false;
1979
+ file.workspaceRestore = structuredClone(transition.restore);
1980
+ parseWorkspaceRestore(file.workspaceRestore);
1981
+ if (Buffer.byteLength(JSON.stringify(file.workspaceRestore)) > transition.maxBytes)
1982
+ throw new Error("Workspace Restore byte capacity reached.");
1983
+ const binding = file.workspaceBindings.find((value) => value.bindingKey === transition.binding.bindingKey);
1984
+ binding.target = { ...transition.nextTarget };
1985
+ binding.updatedAtMs = nowMs;
1986
+ delete binding.displayTitle;
1987
+ record.target = { ...transition.nextTarget };
1988
+ record.updatedAtMs = nowMs;
1989
+ record.rerouteConfirmedAtMs = nowMs;
1990
+ record.syncStatus = "unknown";
1991
+ record.lastReconcileAction = "workspace-restore";
1992
+ delete record.lastError;
1993
+ delete record.lastSyncError;
1994
+ delete record.lastSyncObservedAtMs;
1995
+ delete record.lastSyncProbeAtMs;
1996
+ }
1997
+ else {
1998
+ file.threads = file.threads.filter((candidate) => candidate !== record);
1999
+ for (const binding of file.workspaceBindings) {
2000
+ if (targetMatches(binding.target, transition.target) && binding.inactiveSinceMs === undefined) {
2001
+ binding.inactiveSinceMs = nowMs;
2002
+ }
2003
+ }
2004
+ if (transition.kind === "invalidate") {
2005
+ file.syncObservations = file.syncObservations.filter((observation) => !targetMatches(observation.target, record.target));
2006
+ file.syncObservations.push({
2007
+ target: { ...record.target },
2008
+ syncStatus: "deleted",
2009
+ observedAtMs: nowMs,
2010
+ ...(record.instanceId ? { instanceId: record.instanceId } : {}),
2011
+ ...(record.slot ? { slot: record.slot } : {}),
2012
+ lastSyncError: transition.lastSyncError,
2013
+ lastReconcileAction: "mark-stale",
2014
+ });
1400
2015
  }
1401
2016
  }
1402
- if (transition.kind === "invalidate") {
1403
- file.syncObservations = file.syncObservations.filter((observation) => !targetMatches(observation.target, record.target));
1404
- file.syncObservations.push({
1405
- target: { ...record.target },
1406
- syncStatus: "deleted",
1407
- observedAtMs: nowMs,
1408
- ...(record.instanceId ? { instanceId: record.instanceId } : {}),
1409
- ...(record.slot ? { slot: record.slot } : {}),
1410
- lastSyncError: transition.lastSyncError,
1411
- lastReconcileAction: "mark-stale",
1412
- });
2017
+ }
2018
+ assertWorkspaceRestoreBindingProtection(file);
2019
+ assertRestoreRecoveryProtection(path, file.workspaceRestore?.operations ?? [], file.pendingProvisions ?? []);
2020
+ if (publishWorkspace) {
2021
+ const wire = JSON.parse(JSON.stringify(file));
2022
+ const outcome = publishWorkspace(current => {
2023
+ if (!scopeMatches(submittedPath, submittedProfile) || mutationRevision !== persistedRevision ||
2024
+ transition?.isCurrent() === false || isExactJournalFrame?.() === false)
2025
+ return { value: current, result: false };
2026
+ const semanticCurrent = getTelegramStateSemanticSnapshot(current);
2027
+ if (!isDeepStrictEqual(semanticCurrent, observedWorkspaceSemantic))
2028
+ throw new Error("Workspace canonical snapshot changed before publication.");
2029
+ // A late whole-Workspace replacement cannot erase a concurrent canonical mutation.
2030
+ if (workspaceRestore && !isDeepStrictEqual(parseTopicTargetFile(current).workspaceRestore, expectedRestore))
2031
+ throw new Error("Workspace Restore evidence changed before publication.");
2032
+ assertRestoreEvidenceCurrent(current, true);
2033
+ if (transition?.kind === "relocate") {
2034
+ const slots = captureExternalReservedSlots();
2035
+ if (!slots || slots.includes(transition.binding.slot) || workspaceRetirementCommitInFlight ||
2036
+ Array.from(workspaceClaims.values()).some(value => value.identity.bindingKey === transition.binding.bindingKey || value.identity.slot === transition.binding.slot))
2037
+ return { value: current, result: false };
2038
+ }
2039
+ const unchanged = !transition && isDeepStrictEqual(semanticCurrent, getTelegramStateSemanticSnapshot(wire));
2040
+ return { value: unchanged ? current : wire, result: true };
2041
+ }, { isCurrent: () => scopeMatches(submittedPath, submittedProfile) && mutationRevision === persistedRevision &&
2042
+ (transition?.isCurrent() ?? true) && (isExactJournalFrame?.() ?? true),
2043
+ onPublicationBoundary: transition?.kind === "relocate" ? boundary => {
2044
+ if (boundary !== "before-write")
2045
+ transition.onPublicationBoundary?.(boundary);
2046
+ } : undefined });
2047
+ if (!outcome.committed || !outcome.result)
2048
+ return false;
2049
+ loadedPath = path;
2050
+ loadedProfile = submittedProfile;
2051
+ loaded = true;
2052
+ observedWorkspaceSemantic = getTelegramStateSemanticSnapshot(wire);
2053
+ if (transition) {
2054
+ records = new Map(file.threads.map(record => {
2055
+ const normalized = normalizeRecord(record);
2056
+ return [getRecordOwnerKey(normalized), cloneRecord(normalized)];
2057
+ }));
2058
+ workspaceRestore = structuredClone(file.workspaceRestore);
2059
+ syncObservations = file.syncObservations;
2060
+ workspaceBindings = new Map(file.workspaceBindings.map(binding => [getWorkspaceBindingMapKey(binding), cloneWorkspaceBinding(binding)]));
2061
+ mutationRevision += 1;
1413
2062
  }
2063
+ dirty = false;
2064
+ return true;
1414
2065
  }
1415
2066
  let persistedSemanticSnapshot;
1416
2067
  try {
1417
- persistedSemanticSnapshot = getTelegramStateSemanticSnapshot(JSON.parse(await readFile(path, "utf8")));
2068
+ persistedSemanticSnapshot = getTelegramStateSemanticSnapshot(await readStoreSnapshot(path, !!workspaceRestore, true));
1418
2069
  }
1419
2070
  catch {
1420
- /* missing or invalid snapshots must be replaced */
2071
+ /* Final Restore validation fences missing or unverifiable snapshots before publication. */
1421
2072
  }
1422
2073
  // Normalize optional fields to wire JSON; object key order is not a state change.
1423
2074
  if (!transition && isDeepStrictEqual(persistedSemanticSnapshot, getTelegramStateSemanticSnapshot(JSON.parse(JSON.stringify(file)))) &&
1424
- mutationRevision === persistedRevision &&
1425
- statusRevision === persistedStatusRevision) {
2075
+ mutationRevision === persistedRevision) {
1426
2076
  loadedPath = path;
1427
2077
  loaded = true;
1428
2078
  dirty = false;
1429
2079
  return true;
1430
2080
  }
2081
+ const serialized = `${JSON.stringify(file, null, 2)}\n`;
2082
+ if (file.workspaceRestore && Buffer.byteLength(serialized) > WORKSPACE_SNAPSHOT_MAX_BYTES)
2083
+ throw new Error("Workspace snapshot byte capacity reached.");
1431
2084
  await mkdir(dirname(path), { recursive: true });
1432
2085
  const tempPath = `${path}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
1433
- await writeFile(tempPath, `${JSON.stringify(file, null, 2)}\n`, {
1434
- encoding: "utf8",
1435
- mode: 0o600,
1436
- });
2086
+ await writeFile(tempPath, serialized, { encoding: "utf8", flag: "wx", mode: 0o600 });
1437
2087
  await chmod(tempPath, 0o600);
1438
2088
  try {
1439
2089
  if (transition) {
2090
+ if (transition.kind === "relocate")
2091
+ transition.onPublicationBoundary?.("after-write-before-rename");
1440
2092
  let applied = false;
1441
2093
  const commit = () => {
1442
- if (getPath() !== path || mutationRevision !== persistedRevision ||
1443
- statusRevision !== persistedStatusRevision || !transition.isCurrent())
2094
+ if (!transition.isCurrent())
2095
+ return;
2096
+ if (transition.kind === "relocate") {
2097
+ // Transient claims and external fences do not advance snapshot revisions.
2098
+ const slots = captureExternalReservedSlots();
2099
+ if (!slots || slots.includes(transition.binding.slot) || workspaceRetirementCommitInFlight ||
2100
+ Array.from(workspaceClaims.values()).some((value) => value.identity.bindingKey === transition.binding.bindingKey ||
2101
+ value.identity.slot === transition.binding.slot))
2102
+ return;
2103
+ }
2104
+ if (getPath() !== path || mutationRevision !== persistedRevision)
2105
+ return;
2106
+ // All publishers fence the same retained operation, including after its terminal removal.
2107
+ assertRestoreEvidenceCurrent();
2108
+ if (!transition.isCurrent() || getPath() !== path || mutationRevision !== persistedRevision)
1444
2109
  return;
1445
- // Fence and rename share one synchronous commit boundary. No stale
1446
- // transition enters the live projection before durable commit.
1447
2110
  renameSync(tempPath, path);
1448
2111
  loadedPath = path;
1449
- records = new Map(Array.from(records).filter(([, record]) => !targetMatches(record.target, transition.target)));
2112
+ if (transition.kind === "relocate") {
2113
+ const relocated = file.threads.find((record) => targetMatches(record.target, transition.nextTarget));
2114
+ records.set(getRecordOwnerKey(transition.owner), {
2115
+ ...relocated, profileKey: transition.owner.profileKey,
2116
+ });
2117
+ }
2118
+ else {
2119
+ records = new Map(Array.from(records).filter(([, record]) => !targetMatches(record.target, transition.target)));
2120
+ }
2121
+ workspaceRestore = structuredClone(file.workspaceRestore);
1450
2122
  syncObservations = file.syncObservations;
1451
2123
  workspaceBindings = new Map(file.workspaceBindings.map((binding) => [
1452
2124
  getWorkspaceBindingMapKey(binding), cloneWorkspaceBinding(binding),
@@ -1454,28 +2126,36 @@ export function createTelegramTopicTargetStore(options) {
1454
2126
  mutationRevision += 1;
1455
2127
  dirty = false;
1456
2128
  applied = true;
2129
+ if (transition.kind === "relocate")
2130
+ transition.onPublicationBoundary?.("after-rename");
1457
2131
  };
1458
- if (options.commitPersist)
1459
- options.commitPersist(commit);
1460
- else if (!options.canPersist || options.canPersist())
1461
- commit();
2132
+ withTelegramFileTransaction(`${path}.transaction`, () => {
2133
+ if (commitPersist)
2134
+ commitPersist(commit);
2135
+ else if (!options.canPersist || options.canPersist())
2136
+ commit();
2137
+ });
1462
2138
  if (!applied)
1463
2139
  await unlink(tempPath).catch(() => undefined);
1464
2140
  return applied;
1465
2141
  }
1466
- if (options.commitPersist) {
1467
- const committed = options.commitPersist(() => {
2142
+ if (commitPersist) {
2143
+ const committed = withTelegramFileTransaction(`${path}.transaction`, () => commitPersist(() => {
2144
+ assertRestoreEvidenceCurrent();
1468
2145
  renameSync(tempPath, path);
1469
2146
  chmodSync(path, 0o600);
1470
- });
2147
+ }));
1471
2148
  if (!committed) {
1472
2149
  await loadFromDisk();
1473
2150
  throw new Error("Telegram thread snapshot lost exact transport ownership before commit.");
1474
2151
  }
1475
2152
  }
1476
2153
  else {
1477
- await rename(tempPath, path);
1478
- await chmod(path, 0o600);
2154
+ withTelegramFileTransaction(`${path}.transaction`, () => {
2155
+ assertRestoreEvidenceCurrent();
2156
+ renameSync(tempPath, path);
2157
+ chmodSync(path, 0o600);
2158
+ });
1479
2159
  }
1480
2160
  }
1481
2161
  catch (error) {
@@ -1493,6 +2173,8 @@ export function createTelegramTopicTargetStore(options) {
1493
2173
  };
1494
2174
  return {
1495
2175
  async load() {
2176
+ if (workspaceIO && dirty && !loadedScopeMatches(getPath()))
2177
+ throw new Error("Workspace projection must refresh after scope change.");
1496
2178
  if (dirty)
1497
2179
  return;
1498
2180
  await loadFromDisk();
@@ -1503,7 +2185,9 @@ export function createTelegramTopicTargetStore(options) {
1503
2185
  return refresh;
1504
2186
  },
1505
2187
  async persist() {
1506
- await persistSnapshot();
2188
+ const committed = await persistSnapshot();
2189
+ if (workspaceIO && !committed)
2190
+ throw new Error("Workspace snapshot lost captured publication authority.");
1507
2191
  },
1508
2192
  invalidateTarget(target, isCurrent, lastSyncError) {
1509
2193
  return persistSnapshot({ kind: "invalidate", target, isCurrent, lastSyncError });
@@ -1514,6 +2198,677 @@ export function createTelegramTopicTargetStore(options) {
1514
2198
  return Promise.resolve(false);
1515
2199
  return persistSnapshot({ kind: "detach", owner, target: owner.target, isCurrent });
1516
2200
  },
2201
+ assertWorkspaceRestoreRegistration(candidate) {
2202
+ withRestoreRegistration(candidate);
2203
+ },
2204
+ commitWorkspaceRestoreRegistration: withRestoreRegistration,
2205
+ listTemporaryThreadTargets() {
2206
+ const file = parseTopicTargetFile(readStoreSnapshot(getPath(), false) ?? { version: 1, records: [] });
2207
+ return (file.workspaceRestore?.temporaryThreads ?? []).flatMap(entry => entry.target ? [{ ...entry.target }] : []);
2208
+ },
2209
+ withWorkspaceRestoreSnapshot(expected, observe) {
2210
+ const path = getPath();
2211
+ withStoreTransaction(path, () => {
2212
+ const file = parseTopicTargetFile(readStoreSnapshot(path, false));
2213
+ observeWorkspaceRestore(file.workspaceRestore);
2214
+ if (getPath() !== path || !isDeepStrictEqual(file.workspaceRestore?.operations.find(intent => intent.request.operationId === expected.request.operationId), expected))
2215
+ throw new Error("Workspace Restore observation changed.");
2216
+ assertWorkspaceRestoreBindingProtection(file);
2217
+ assertRestoreRecoveryProtection(path, file.workspaceRestore.operations, file.pendingProvisions ?? []);
2218
+ observe({ threads: file.threads, workspaceBindings: file.workspaceBindings });
2219
+ });
2220
+ },
2221
+ workspaceRestore(storageOptions) {
2222
+ const path = getPath(), profile = activeProfile();
2223
+ const now = storageOptions.getNowMs ?? Date.now;
2224
+ const { profileName, tokenSha256, isCurrentScope, legacyPath, onPublicationBoundary } = storageOptions;
2225
+ if (consolidated && profileName !== profile)
2226
+ throw new Error("Foreign Workspace Restore storage scope.");
2227
+ const maxBytes = Math.min(storageOptions.maxBytes ?? WORKSPACE_RESTORE_MAX_BYTES, WORKSPACE_RESTORE_MAX_BYTES);
2228
+ if (!restoreText(profileName) || !/^[a-f0-9]{64}$/u.test(tokenSha256) || !Number.isSafeInteger(maxBytes) || maxBytes <= 0)
2229
+ throw new Error("Invalid Workspace Restore storage scope.");
2230
+ const empty = { version: 1, profileName, tokenSha256, revision: 0, operations: [] };
2231
+ let observed;
2232
+ const currentScope = () => getPath() === path && (!consolidated || scopeMatches(path, profile)) && (isCurrentScope?.() ?? true) && (options.canPersist?.() ?? true);
2233
+ const read = () => {
2234
+ if (legacyPath) {
2235
+ try {
2236
+ lstatSync(legacyPath);
2237
+ throw new Error("Unmigrated Workspace Restore file.");
2238
+ }
2239
+ catch (error) {
2240
+ if (error.code !== "ENOENT")
2241
+ throw error;
2242
+ }
2243
+ }
2244
+ const raw = readStoreSnapshot(path);
2245
+ if (raw !== undefined && (!restoreObject(raw) || raw.version !== 1))
2246
+ throw new Error("Invalid Workspace snapshot.");
2247
+ const file = (raw ?? (consolidated ? { version: 1, source: "snapshot", writtenAtMs: 0, bot: { threadMode: "unknown" }, threads: [] }
2248
+ : { version: 1, records: [] }));
2249
+ const state = parseTopicTargetFile(file).workspaceRestore;
2250
+ if (state && (state.profileName !== profileName || state.tokenSha256 !== tokenSha256))
2251
+ throw new Error("Invalid or foreign Workspace Restore evidence.");
2252
+ if (observed && (!state || state.revision < observed.revision ||
2253
+ (state.revision === observed.revision && !isDeepStrictEqual(state, observed))))
2254
+ throw new Error("Workspace Restore evidence moved backwards or changed without revision.");
2255
+ if (state && Buffer.byteLength(JSON.stringify(state)) > maxBytes)
2256
+ throw new Error("Workspace Restore byte capacity reached.");
2257
+ observed = structuredClone(state);
2258
+ if (loadedScopeMatches(path))
2259
+ observeWorkspaceRestore(state);
2260
+ return { file, state: structuredClone(state ?? empty) };
2261
+ };
2262
+ const inspectTemporaryOwnership = (before, entry) => {
2263
+ if (entry.phase !== "created" || !entry.target)
2264
+ return { kind: "unknown" };
2265
+ const file = parseTopicTargetFile(before.file);
2266
+ // Normalization omissions never prove an unbound target.
2267
+ for (const key of ["threads", "workspaceBindings", "reservations", "pendingProvisions", "pendingCleanups", "workspaceRetirements"]) {
2268
+ const raw = before.file[key];
2269
+ if ((key === "threads" && raw === undefined) || (raw !== undefined &&
2270
+ (!Array.isArray(raw) || raw.length !== (file[key]?.length ?? 0))))
2271
+ throw new Error("Invalid temporary Thread target evidence.");
2272
+ }
2273
+ assertWorkspaceRestoreBindingProtection(file);
2274
+ assertRestoreRecoveryProtection(path, before.state.operations, file.pendingProvisions ?? [], true);
2275
+ const target = entry.target;
2276
+ const bindings = (file.workspaceBindings ?? []).filter(value => targetMatches(value.target, target));
2277
+ if (bindings.length === 1)
2278
+ return { kind: "bound", binding: structuredClone(bindings[0]) };
2279
+ if (bindings.length > 1 || file.threads.some(value => targetMatches(value.target, target)) ||
2280
+ file.reservations?.some(value => targetMatches(value.target, target)) ||
2281
+ file.pendingProvisions?.some(value => !value.target || targetMatches(value.target, target)) ||
2282
+ file.pendingCleanups?.some(value => targetMatches(value.target, target)) ||
2283
+ file.workspaceRetirements?.some(value => targetMatches(value.binding.target, target)) ||
2284
+ before.state.operations.some(value => targetMatches(value.request.target, target)))
2285
+ return { kind: "unknown" };
2286
+ return { kind: "temporary" };
2287
+ };
2288
+ const classifyTemporaryTarget = (before, entry) => entry.cleanupIssued ? { kind: "unknown" } : inspectTemporaryOwnership(before, entry);
2289
+ const storage = {
2290
+ read: () => read().state,
2291
+ async relocate(input, isCurrent) {
2292
+ if (!currentScope() || !isCurrent())
2293
+ return false;
2294
+ const intent = structuredClone(input);
2295
+ if (!currentScope() || !isCurrent() || !isWorkspaceRestoreIntent(intent) || intent.phase !== "relocated" || intent.revision !== 0 ||
2296
+ intent.recipient !== undefined || intent.routing !== undefined)
2297
+ return false;
2298
+ const state = read().state;
2299
+ if (state.operations.some(operation => workspaceRestoresConflict(operation.request, intent.request)))
2300
+ return false;
2301
+ if (state.revision === Number.MAX_SAFE_INTEGER)
2302
+ throw new Error("Workspace Restore revision exhausted.");
2303
+ if (state.operations.length >= 26)
2304
+ throw new Error("Workspace Restore operation capacity reached.");
2305
+ const restore = { ...state, revision: state.revision + 1, operations: [...state.operations, intent] };
2306
+ const request = intent.request;
2307
+ return persistSnapshot({ kind: "relocate", operationId: request.operationId, binding: request.binding, owner: request.owner,
2308
+ target: { ...request.binding.target }, nextTarget: { ...request.target }, restore, maxBytes, onPublicationBoundary,
2309
+ isCurrent: () => currentScope() && isCurrent() });
2310
+ },
2311
+ update(expectedInput, nextInput, isCurrent) {
2312
+ const expected = structuredClone(expectedInput), next = structuredClone(nextInput);
2313
+ if (!currentScope() || !isCurrent())
2314
+ return false;
2315
+ if (expected.revision === Number.MAX_SAFE_INTEGER)
2316
+ throw new Error("Workspace Restore revision exhausted.");
2317
+ if (next.profileName !== profileName || next.tokenSha256 !== tokenSha256 || next.revision !== expected.revision + 1 || !parseWorkspaceRestore(next))
2318
+ throw new Error("Invalid Workspace Restore publication.");
2319
+ if (Buffer.byteLength(JSON.stringify(next)) > maxBytes)
2320
+ throw new Error("Workspace Restore byte capacity reached.");
2321
+ if (!workspaceIO)
2322
+ mkdirSync(dirname(path), { recursive: true });
2323
+ let committedBefore;
2324
+ let committedAfter;
2325
+ const committed = withStoreTransaction(path, () => {
2326
+ const before = read();
2327
+ if (!isDeepStrictEqual(before.state, expected) || !currentScope() || !isCurrent())
2328
+ return false;
2329
+ for (const entry of next.temporaryThreads ?? []) {
2330
+ const prior = expected.temporaryThreads?.find(value => value.token === entry.token);
2331
+ if (!prior && entry.phase === "created" && classifyTemporaryTarget(before, entry).kind !== "temporary")
2332
+ return false;
2333
+ if (!entry.cleanupIssued)
2334
+ continue;
2335
+ if (!prior?.cleanupIssued && (!prior || classifyTemporaryTarget(before, prior).kind !== "temporary"))
2336
+ return false;
2337
+ }
2338
+ assertWorkspaceRestoreBindingProtection(before.file);
2339
+ const provisions = parseTopicTargetFile(before.file).pendingProvisions ?? [];
2340
+ assertRestoreRecoveryProtection(path, before.state.operations, provisions);
2341
+ const after = { ...before.file, workspaceRestore: next };
2342
+ // New temporary targets must be checked against the file they will be published into.
2343
+ assertWorkspaceRestoreBindingProtection(parseTopicTargetFile(after));
2344
+ const content = JSON.stringify(after);
2345
+ if (Buffer.byteLength(content) > WORKSPACE_SNAPSHOT_MAX_BYTES)
2346
+ throw new Error("Workspace snapshot byte capacity reached.");
2347
+ if (workspaceIO) {
2348
+ if (!workspaceFrame || !currentScope() || !isCurrent())
2349
+ return false;
2350
+ assertRestoreRecoveryProtection(path, [...before.state.operations, ...next.operations], provisions);
2351
+ committedBefore = before;
2352
+ committedAfter = JSON.parse(content);
2353
+ workspaceFrame.value = committedAfter;
2354
+ return true;
2355
+ }
2356
+ const temporary = `${path}.${randomUUID()}.tmp`;
2357
+ try {
2358
+ writeFileSync(temporary, content, { encoding: "utf8", flag: "wx", mode: 0o600 });
2359
+ onPublicationBoundary?.("after-write-before-rename");
2360
+ let published = false;
2361
+ const publish = () => {
2362
+ if (!currentScope() || !isCurrent())
2363
+ return;
2364
+ if (!isDeepStrictEqual(read().file, before.file))
2365
+ throw new Error("Workspace Restore evidence changed before publication.");
2366
+ assertRestoreRecoveryProtection(path, [...before.state.operations, ...next.operations], provisions);
2367
+ if (!currentScope() || !isCurrent())
2368
+ return;
2369
+ renameSync(temporary, path);
2370
+ observed = structuredClone(next);
2371
+ // A read-only Restore view must never bless a stale binding/owner projection.
2372
+ if (loadedPath === path && isDeepStrictEqual(workspaceRestore, before.file.workspaceRestore))
2373
+ workspaceRestore = structuredClone(next);
2374
+ mutationRevision += 1;
2375
+ published = true;
2376
+ onPublicationBoundary?.("after-rename");
2377
+ };
2378
+ const owned = commitPersist ? commitPersist(publish) : (publish(), true);
2379
+ return owned && published;
2380
+ }
2381
+ finally {
2382
+ try {
2383
+ unlinkSync(temporary);
2384
+ }
2385
+ catch (error) {
2386
+ if (error.code !== "ENOENT")
2387
+ throw error;
2388
+ }
2389
+ }
2390
+ }, { isCurrent: () => currentScope() && isCurrent(), onPublicationBoundary: boundary => {
2391
+ if (boundary !== "before-write")
2392
+ onPublicationBoundary?.(boundary);
2393
+ } });
2394
+ if (workspaceIO && committed && committedBefore && committedAfter) {
2395
+ observed = structuredClone(next);
2396
+ if (loadedScopeMatches(path) && isDeepStrictEqual(workspaceRestore, committedBefore.file.workspaceRestore))
2397
+ workspaceRestore = structuredClone(next);
2398
+ if (isDeepStrictEqual(observedWorkspaceSemantic, getTelegramStateSemanticSnapshot(committedBefore.file)))
2399
+ observedWorkspaceSemantic = getTelegramStateSemanticSnapshot(committedAfter);
2400
+ mutationRevision += 1;
2401
+ }
2402
+ return committed;
2403
+ },
2404
+ };
2405
+ const mutate = (authority, change) => {
2406
+ if (!authority.isCurrent())
2407
+ return undefined;
2408
+ const executor = structuredClone(authority.executor);
2409
+ const operator = authority.operatorUserId;
2410
+ if (!isTelegramWorkspaceRestoreExecutor(executor) || !restoreInteger(operator))
2411
+ return undefined;
2412
+ const current = () => authority.isCurrent() && authority.operatorUserId === operator && isDeepStrictEqual(authority.executor, executor);
2413
+ const before = storage.read();
2414
+ const file = structuredClone(before);
2415
+ const operation = change(file, executor, operator);
2416
+ if (!operation || !current())
2417
+ return undefined;
2418
+ if (isDeepStrictEqual(file, before))
2419
+ return structuredClone(operation);
2420
+ if (file.revision === Number.MAX_SAFE_INTEGER)
2421
+ throw new Error("Workspace Restore revision exhausted.");
2422
+ file.revision += 1;
2423
+ return storage.update(before, file, current) ? structuredClone(operation) : undefined;
2424
+ };
2425
+ const advance = (expected, authority, change, adopt = false) => mutate(authority, (file, executor, operator) => {
2426
+ const operation = file.operations.find(candidate => candidate.request.operationId === expected.request.operationId);
2427
+ if (!operation || !isDeepStrictEqual(operation, expected) || operation.operatorUserId !== operator ||
2428
+ (!adopt && !isDeepStrictEqual(operation.executor, executor)))
2429
+ return undefined;
2430
+ const changed = change(operation);
2431
+ if (!changed)
2432
+ return undefined;
2433
+ if (changed === "unchanged")
2434
+ return operation;
2435
+ if (operation.revision === Number.MAX_SAFE_INTEGER)
2436
+ throw new Error("Workspace Restore operation revision exhausted.");
2437
+ const at = now();
2438
+ if (!restoreInteger(at))
2439
+ throw new Error("Invalid Workspace Restore clock.");
2440
+ operation.executor = executor;
2441
+ operation.revision += 1;
2442
+ operation.updatedAtMs = Math.max(at, operation.updatedAtMs);
2443
+ return operation;
2444
+ });
2445
+ const advanceTemporary = (expected, authority, change, adopt = false) => mutate(authority, (file, executor, operator) => {
2446
+ const entry = file.temporaryThreads?.find(value => isDeepStrictEqual(value, expected));
2447
+ if (!entry || entry.operatorUserId !== operator || (!adopt && !isDeepStrictEqual(entry.executor, executor)))
2448
+ return undefined;
2449
+ const changed = change(entry, file, executor);
2450
+ if (!changed)
2451
+ return undefined;
2452
+ if (changed === "unchanged")
2453
+ return entry;
2454
+ if (entry.revision === Number.MAX_SAFE_INTEGER)
2455
+ throw new Error("Workspace Restore operation revision exhausted.");
2456
+ const at = now();
2457
+ if (!restoreInteger(at))
2458
+ throw new Error("Invalid Workspace Restore clock.");
2459
+ entry.executor = executor;
2460
+ entry.revision += 1;
2461
+ entry.updatedAtMs = Math.max(at, entry.updatedAtMs);
2462
+ return entry;
2463
+ });
2464
+ return {
2465
+ list: () => structuredClone(storage.read().operations),
2466
+ inspectTemporaryThreadTarget(expected, authority) {
2467
+ const executor = structuredClone(authority.executor), operator = authority.operatorUserId;
2468
+ const current = () => currentScope() && authority.isCurrent() && authority.operatorUserId === operator &&
2469
+ isDeepStrictEqual(authority.executor, executor);
2470
+ if (!isTelegramWorkspaceRestoreExecutor(executor) || !restoreInteger(operator) || !current())
2471
+ return undefined;
2472
+ return withStoreTransaction(path, () => {
2473
+ if (!current())
2474
+ return undefined;
2475
+ const before = read(), entry = before.state.temporaryThreads?.find(value => isDeepStrictEqual(value, expected));
2476
+ if (!entry || entry.operatorUserId !== operator || !current())
2477
+ return undefined;
2478
+ const result = classifyTemporaryTarget(before, entry);
2479
+ return current() ? result : undefined;
2480
+ });
2481
+ },
2482
+ isTemporaryThreadCleanupCurrent(expected, authority) {
2483
+ const executor = structuredClone(authority.executor), operator = authority.operatorUserId;
2484
+ const current = () => currentScope() && authority.isCurrent() && authority.operatorUserId === operator &&
2485
+ isDeepStrictEqual(authority.executor, executor);
2486
+ if (!expected.cleanupIssued || expected.operatorUserId !== operator || !isDeepStrictEqual(expected.executor, executor) ||
2487
+ !isTelegramWorkspaceRestoreExecutor(executor) || !restoreInteger(operator) || !current())
2488
+ return false;
2489
+ return withStoreTransaction(path, () => {
2490
+ if (!current())
2491
+ return false;
2492
+ const before = read(), entry = before.state.temporaryThreads?.find(value => isDeepStrictEqual(value, expected));
2493
+ return !!entry && inspectTemporaryOwnership(before, entry).kind === "temporary" && current();
2494
+ });
2495
+ },
2496
+ /** The first durable state already includes both relocation and exact original references. */
2497
+ async commit(requestInput, authority) {
2498
+ if (!authority.isCurrent())
2499
+ return undefined;
2500
+ const request = structuredClone(requestInput), executor = structuredClone(authority.executor), operatorUserId = authority.operatorUserId;
2501
+ const current = () => authority.isCurrent() && authority.operatorUserId === operatorUserId && isDeepStrictEqual(authority.executor, executor);
2502
+ if (!current() || !isTelegramWorkspaceRestoreRequest(request) || !isTelegramWorkspaceRestoreExecutor(executor) ||
2503
+ !restoreInteger(operatorUserId) || request.target.chatId !== operatorUserId)
2504
+ return undefined;
2505
+ const retained = () => {
2506
+ if (!current())
2507
+ return undefined;
2508
+ const operation = storage.read().operations.find(value => value.request.operationId === request.operationId);
2509
+ return current() && operation?.phase === "relocated" && isDeepStrictEqual(operation.request, request) &&
2510
+ operation.operatorUserId === operatorUserId && isDeepStrictEqual(operation.executor, executor) ? operation : undefined;
2511
+ };
2512
+ const existing = retained();
2513
+ if (existing)
2514
+ return existing;
2515
+ await (dirty ? undefined : loadFromDisk());
2516
+ if (!current())
2517
+ return undefined;
2518
+ const at = now();
2519
+ if (!restoreInteger(at))
2520
+ throw new Error("Invalid Workspace Restore clock.");
2521
+ const intent = { request, executor, operatorUserId, revision: 0,
2522
+ createdAtMs: at, updatedAtMs: at, committedAtMs: at, phase: "relocated" };
2523
+ let failure;
2524
+ try {
2525
+ await storage.relocate(intent, current);
2526
+ }
2527
+ catch (error) {
2528
+ failure = { error };
2529
+ }
2530
+ if (!current())
2531
+ return undefined;
2532
+ // A lost rename reply is reconciled from the complete operation, never target equality.
2533
+ const committed = retained();
2534
+ if (!committed && failure)
2535
+ throw failure.error;
2536
+ return committed;
2537
+ },
2538
+ adopt(expected, authority) {
2539
+ return advance(expected, authority, () => !isDeepStrictEqual(expected.executor, authority.executor), true);
2540
+ },
2541
+ /** Caller validates the live recipient under admission. Only this fresh result grants one RPC issuance. */
2542
+ issueRecipient(expected, recipient, authority) {
2543
+ const intent = advance(expected, authority, operation => {
2544
+ if (operation.phase !== "relocated" || !isTelegramWorkspaceRestoreRecipient(recipient) || recipient.sessionId !== operation.request.binding.sessionId)
2545
+ return false;
2546
+ operation.phase = "recipient-issued";
2547
+ operation.recipient = structuredClone(recipient);
2548
+ return true;
2549
+ });
2550
+ return intent ? { issued: true, intent } : undefined;
2551
+ },
2552
+ /** One source-dispatch grant; uncertainty never resets it. Caller retains the original carriers. */
2553
+ issueRouting(expected, authority) {
2554
+ const intent = advance(expected, authority, operation => {
2555
+ if (operation.phase !== "ready" || operation.routing)
2556
+ return false;
2557
+ operation.routing = { settlements: [] };
2558
+ return true;
2559
+ });
2560
+ return intent ? { issued: true, intent } : undefined;
2561
+ },
2562
+ /** Caller owns the positive result and exact source hash; this grants neither source removal nor cleanup. */
2563
+ recordSourceAcceptance(expected, evidenceInput, authority) {
2564
+ if (!authority.isCurrent())
2565
+ return undefined;
2566
+ const evidence = structuredClone(evidenceInput);
2567
+ return advance(expected, authority, operation => {
2568
+ if (!operation.routing || !isWorkspaceRestoreSourceAcceptance(evidence, operation))
2569
+ return false;
2570
+ const existing = operation.routing.acceptances?.find(value => value.updateId === evidence.updateId);
2571
+ if (existing)
2572
+ return isDeepStrictEqual(existing, evidence) ? "unchanged" : false;
2573
+ if (operation.routing.cleanup !== undefined ||
2574
+ operation.routing.settlements.some(value => value.updateIds.includes(evidence.updateId)) ||
2575
+ !isDeepStrictEqual(evidence.recipient, operation.readyRecipient ?? operation.recipient))
2576
+ return false;
2577
+ const acceptances = [...operation.routing.acceptances ?? [], evidence].sort((a, b) => a.updateId - b.updateId);
2578
+ if (!workspaceRestoreAcceptancesConsistent(acceptances))
2579
+ return false;
2580
+ operation.routing.acceptances = acceptances;
2581
+ return true;
2582
+ });
2583
+ },
2584
+ /** Queue admission is nonterminal; only its exact journal-owner completion ACK may upgrade it. */
2585
+ recordSourceSettlement(expected, evidenceInput, authority) {
2586
+ const evidence = structuredClone(evidenceInput);
2587
+ return advance(expected, authority, operation => {
2588
+ if (!operation.routing || !isTelegramWorkspaceRestoreSettlement(evidence, operation.request.source) ||
2589
+ !workspaceRestoreSettlementMatchesAcceptance(evidence, operation.routing.acceptances ?? []))
2590
+ return false;
2591
+ const overlaps = operation.routing.settlements.filter(value => value.updateIds.some(id => evidence.updateIds.includes(id)));
2592
+ if (overlaps.some(value => evidence.kind !== "queue-completed" || value.kind !== "queued" ||
2593
+ value.receiptId !== evidence.receiptId || value.queueKind !== evidence.queueKind))
2594
+ return false;
2595
+ operation.routing.settlements = operation.routing.settlements.flatMap(value => {
2596
+ const updateIds = value.updateIds.filter(id => !evidence.updateIds.includes(id));
2597
+ return updateIds.length ? [{ ...value, updateIds }] : [];
2598
+ });
2599
+ operation.routing.settlements.push(evidence);
2600
+ return true;
2601
+ });
2602
+ },
2603
+ /** Existing cleanup owner retains deletion authority; this merely fences one invocation. */
2604
+ issueCleanup(expected, authority) {
2605
+ const intent = advance(expected, authority, operation => {
2606
+ if (!operation.routing || !workspaceRestoreSourcesSettled(operation) || operation.routing.cleanup !== undefined)
2607
+ return false;
2608
+ operation.routing.cleanup = "issued";
2609
+ return true;
2610
+ });
2611
+ return intent ? { issued: true, intent } : undefined;
2612
+ },
2613
+ recordCleanup(expected, result, authority) {
2614
+ return advance(expected, authority, operation => {
2615
+ if (!operation.routing || !workspaceRestoreSourcesSettled(operation) || !isDeepStrictEqual(result.target, operation.request.binding.target) ||
2616
+ (result.kind === "completed" ? operation.routing.cleanup !== "issued" :
2617
+ result.kind !== "not-issued" || operation.routing.cleanup !== undefined))
2618
+ return false;
2619
+ operation.routing.cleanup = result.kind;
2620
+ return true;
2621
+ });
2622
+ },
2623
+ /** Exact terminal CAS releases Restore protection only; it cannot settle or cancel recipient queue work. */
2624
+ retire(expected, authority) {
2625
+ return mutate(authority, (file, executor, operator) => {
2626
+ const index = file.operations.findIndex(value => value.request.operationId === expected.request.operationId);
2627
+ const operation = file.operations[index];
2628
+ if (!operation || !isDeepStrictEqual(operation, expected) || operation.operatorUserId !== operator ||
2629
+ !isDeepStrictEqual(operation.executor, executor) || !workspaceRestoreSourcesSettled(operation) ||
2630
+ !["completed", "not-issued"].includes(operation.routing?.cleanup ?? ""))
2631
+ return undefined;
2632
+ file.operations.splice(index, 1);
2633
+ return operation;
2634
+ });
2635
+ },
2636
+ listTemporaryThreads: () => structuredClone(storage.read().temporaryThreads ?? []),
2637
+ reserveTemporaryThread(sourceInput, token, authority) {
2638
+ const source = structuredClone(sourceInput);
2639
+ let reserved = false;
2640
+ const entry = mutate(authority, (file, executor, operator) => {
2641
+ const entries = file.temporaryThreads ?? [];
2642
+ const existing = entries.find(value => value.source.journalBindingKey === source.journalBindingKey &&
2643
+ value.source.updateId === source.updateId);
2644
+ // A retained entry proves an earlier attempt; it is never a license for another creation.
2645
+ if (existing)
2646
+ return existing.operatorUserId === operator ? existing : undefined;
2647
+ const at = now();
2648
+ if (!restoreInteger(at))
2649
+ throw new Error("Invalid Workspace Restore clock.");
2650
+ const next = { source,
2651
+ inputs: [{ journalBindingKey: source.journalBindingKey, updateIds: [source.updateId] }],
2652
+ operatorUserId: operator, executor, token, phase: "creating", forwardProtocol: "one-shot-v1",
2653
+ revision: 0, createdAtMs: at, updatedAtMs: at };
2654
+ if (!isTemporaryThreadEntry(next) || entries.some(value => value.token === token ||
2655
+ getTelegramTemporaryThreadInputs(value).some(input => input.journalBindingKey === source.journalBindingKey &&
2656
+ input.updateIds.includes(source.updateId))))
2657
+ return undefined;
2658
+ if (entries.length >= TEMPORARY_THREAD_CAPACITY)
2659
+ throw new Error("Temporary Thread capacity reached.");
2660
+ file.temporaryThreads = [...entries, next];
2661
+ reserved = true;
2662
+ return next;
2663
+ });
2664
+ return entry ? { reserved, entry } : undefined;
2665
+ },
2666
+ registerImplicitTemporaryThread(inputValue, targetInput, token, authority) {
2667
+ const input = structuredClone(inputValue), target = { chatId: targetInput.chatId, threadId: targetInput.threadId };
2668
+ if (!isTemporaryThreadInput(input) || input.updateIds.length !== 1 || !Number.isSafeInteger(target.threadId) || target.threadId <= 0)
2669
+ return undefined;
2670
+ return mutate(authority, (file, executor, operator) => {
2671
+ const entries = file.temporaryThreads ?? [];
2672
+ if (target.chatId !== operator || entries.some(value => value.token === token ||
2673
+ (value.target && targetMatches(value.target, target)) || getTelegramTemporaryThreadInputs(value).some(other => other.journalBindingKey === input.journalBindingKey && other.updateIds.some(id => input.updateIds.includes(id)))))
2674
+ return undefined;
2675
+ const at = now();
2676
+ if (!restoreInteger(at))
2677
+ throw new Error("Invalid Workspace Restore clock.");
2678
+ const entry = { source: { journalBindingKey: input.journalBindingKey, updateId: input.updateIds[0] },
2679
+ inputs: [input], target, operatorUserId: operator, executor, token, phase: "created", forwardProtocol: "one-shot-v1",
2680
+ revision: 0, createdAtMs: at, updatedAtMs: at };
2681
+ if (!isTemporaryThreadEntry(entry))
2682
+ return undefined;
2683
+ if (entries.length >= TEMPORARY_THREAD_CAPACITY)
2684
+ throw new Error("Temporary Thread capacity reached.");
2685
+ file.temporaryThreads = [...entries, entry];
2686
+ return entry;
2687
+ });
2688
+ },
2689
+ acknowledgeTemporaryThread(expected, targetInput, authority) {
2690
+ const target = { chatId: targetInput.chatId, threadId: targetInput.threadId };
2691
+ return advanceTemporary(expected, authority, (entry, file) => {
2692
+ if (entry.phase !== "creating" || target.chatId !== entry.operatorUserId || !Number.isSafeInteger(target.threadId) ||
2693
+ target.threadId <= 0 || file.temporaryThreads.some(value => !!value.target && targetMatches(value.target, target)) ||
2694
+ file.operations.some(({ request }) => targetMatches(request.target, target) || targetMatches(request.binding.target, target)))
2695
+ return false;
2696
+ entry.phase = "created";
2697
+ entry.target = target;
2698
+ return true;
2699
+ });
2700
+ },
2701
+ adoptTemporaryThread(expected, authority) {
2702
+ return advanceTemporary(expected, authority, (entry, _file, executor) => !isDeepStrictEqual(entry.executor, executor), true);
2703
+ },
2704
+ recordTemporaryThreadInput(expected, inputValue, authority) {
2705
+ const input = structuredClone(inputValue);
2706
+ if (!isTemporaryThreadInput(input))
2707
+ return undefined;
2708
+ return advanceTemporary(expected, authority, (entry, file) => {
2709
+ if (entry.phase !== "created" || input.journalBindingKey !== entry.source.journalBindingKey ||
2710
+ file.operations.some(({ request }) => targetMatches(request.target, entry.target)))
2711
+ return false;
2712
+ const inputs = getTelegramTemporaryThreadInputs(entry);
2713
+ if (inputs.some(value => isDeepStrictEqual(value, input)))
2714
+ return "unchanged";
2715
+ if (entry.cleanupIssued || inputs.some(value => value.updateIds.some(id => input.updateIds.includes(id))) ||
2716
+ file.temporaryThreads.some(value => value !== entry && getTelegramTemporaryThreadInputs(value).some(other => other.journalBindingKey === input.journalBindingKey && other.updateIds.some(id => input.updateIds.includes(id)))))
2717
+ return false;
2718
+ if (inputs.length >= TEMPORARY_THREAD_INPUT_CAPACITY)
2719
+ throw new Error("Temporary Thread input capacity reached.");
2720
+ entry.inputs = [...inputs, input];
2721
+ return true;
2722
+ });
2723
+ },
2724
+ recordTemporaryThreadInputCancellation(expected, inputValue, authority, inspect) {
2725
+ const input = structuredClone(inputValue);
2726
+ if (!isTemporaryThreadInput(input) || typeof inspect !== "function")
2727
+ return undefined;
2728
+ const operatorUserId = authority.operatorUserId, executor = structuredClone(authority.executor);
2729
+ const owned = () => authority.isCurrent() && authority.operatorUserId === operatorUserId &&
2730
+ isDeepStrictEqual(authority.executor, executor);
2731
+ // Every publication fence observes retained proof again; neither cached cancellation nor absence suffices.
2732
+ const current = () => owned() && input.updateIds.every(updateId => {
2733
+ const evidence = inspect(updateId);
2734
+ return evidence?.journalBindingKey === input.journalBindingKey && evidence.updateId === updateId &&
2735
+ evidence.operatorAuthorityId === `telegram-owner:${operatorUserId}`;
2736
+ }) && owned();
2737
+ const recorded = advanceTemporary(expected, { ...authority, isCurrent: current }, (entry, file) => {
2738
+ if (entry.phase !== "created" || !getTelegramTemporaryThreadInputs(entry).some(candidate => isDeepStrictEqual(candidate, input)) ||
2739
+ file.operations.some(({ request }) => request.source.journalBindingKey === input.journalBindingKey &&
2740
+ request.source.updateIds.some(id => input.updateIds.includes(id))))
2741
+ return false;
2742
+ const cancelled = entry.cancelledInputs ?? [];
2743
+ if (cancelled.some(candidate => isDeepStrictEqual(candidate, input)))
2744
+ return "unchanged";
2745
+ if (entry.completedInputs?.some(candidate => isDeepStrictEqual(candidate, input)) ||
2746
+ entry.forwardedInputs?.some(candidate => isDeepStrictEqual(candidate, input)))
2747
+ return false;
2748
+ entry.cancelledInputs = [...cancelled, input];
2749
+ return true;
2750
+ });
2751
+ return recorded && current() ? recorded : undefined;
2752
+ },
2753
+ recordTemporaryThreadForwardIssued(expected, inputValue, authority) {
2754
+ const input = structuredClone(inputValue);
2755
+ if (!isTemporaryThreadInput(input))
2756
+ return undefined;
2757
+ return advanceTemporary(expected, authority, (entry, file) => {
2758
+ if (entry.phase !== "created" || !getTelegramTemporaryThreadInputs(entry).some(candidate => isDeepStrictEqual(candidate, input)) ||
2759
+ [...(entry.cancelledInputs ?? []), ...(entry.completedInputs ?? []), ...(entry.forwardedInputs ?? [])]
2760
+ .some(candidate => isDeepStrictEqual(candidate, input)) ||
2761
+ file.operations.some(({ request }) => request.source.journalBindingKey === input.journalBindingKey &&
2762
+ request.source.updateIds.some(id => input.updateIds.includes(id))))
2763
+ return false;
2764
+ entry.forwardedInputs = [...(entry.forwardedInputs ?? []), input];
2765
+ return true;
2766
+ });
2767
+ },
2768
+ recordTemporaryThreadInputCompletion(expected, inputValue, authority) {
2769
+ const input = structuredClone(inputValue);
2770
+ if (!isTemporaryThreadInput(input))
2771
+ return undefined;
2772
+ return advanceTemporary(expected, authority, (entry, file) => {
2773
+ if (entry.phase !== "created" || !getTelegramTemporaryThreadInputs(entry).some(candidate => isDeepStrictEqual(candidate, input)) ||
2774
+ entry.cancelledInputs?.some(candidate => isDeepStrictEqual(candidate, input)) ||
2775
+ file.operations.some(({ request }) => request.source.journalBindingKey === input.journalBindingKey &&
2776
+ request.source.updateIds.some(id => input.updateIds.includes(id))))
2777
+ return false;
2778
+ const completed = entry.completedInputs ?? [];
2779
+ if (completed.some(candidate => isDeepStrictEqual(candidate, input)))
2780
+ return "unchanged";
2781
+ entry.completedInputs = [...completed, input];
2782
+ return true;
2783
+ });
2784
+ },
2785
+ issueTemporaryThreadCleanup(expected, authority) {
2786
+ const entry = advanceTemporary(expected, authority, value => {
2787
+ if (value.phase !== "created" || value.cleanupIssued || !isTelegramTemporaryThreadFullyResolved(value))
2788
+ return false;
2789
+ value.cleanupIssued = true;
2790
+ return true;
2791
+ });
2792
+ return entry ? { issued: true, entry } : undefined;
2793
+ },
2794
+ retireTemporaryThread(expected, authority, completed) {
2795
+ return mutate(authority, (file, executor, operator) => {
2796
+ const entries = file.temporaryThreads ?? [];
2797
+ const index = entries.findIndex(value => isDeepStrictEqual(value, expected));
2798
+ const entry = entries[index];
2799
+ if (!entry || entry.operatorUserId !== operator || !isDeepStrictEqual(entry.executor, executor) ||
2800
+ (getTelegramTemporaryThreadInputs(entry).length > 1 && !isTelegramTemporaryThreadFullyResolved(entry) &&
2801
+ !isTemporaryThreadReleasedByCompletion(entry, completed)))
2802
+ return undefined;
2803
+ const remaining = entries.filter((_value, position) => position !== index);
2804
+ if (remaining.length)
2805
+ file.temporaryThreads = remaining;
2806
+ else
2807
+ delete file.temporaryThreads;
2808
+ return entry;
2809
+ });
2810
+ },
2811
+ forgetPreviousWorld(authority) {
2812
+ if (!authority.isCurrent())
2813
+ return undefined;
2814
+ const executor = structuredClone(authority.executor), operator = authority.operatorUserId;
2815
+ if (!isTelegramWorkspaceRestoreExecutor(executor) || !restoreInteger(operator))
2816
+ return undefined;
2817
+ const current = () => authority.isCurrent() && authority.operatorUserId === operator && isDeepStrictEqual(authority.executor, executor);
2818
+ const previous = (value) => value.operatorUserId === operator && value.executor.instanceId !== executor.instanceId;
2819
+ const before = storage.read(), file = structuredClone(before);
2820
+ const operations = file.operations.filter(previous), temporaryThreads = (file.temporaryThreads ?? []).filter(previous);
2821
+ if (!current())
2822
+ return undefined;
2823
+ if (!operations.length && !temporaryThreads.length)
2824
+ return { operations, temporaryThreads };
2825
+ file.operations = file.operations.filter(value => !previous(value));
2826
+ const remaining = (file.temporaryThreads ?? []).filter(value => !previous(value));
2827
+ if (remaining.length)
2828
+ file.temporaryThreads = remaining;
2829
+ else
2830
+ delete file.temporaryThreads;
2831
+ if (file.revision === Number.MAX_SAFE_INTEGER)
2832
+ throw new Error("Workspace Restore revision exhausted.");
2833
+ file.revision += 1;
2834
+ return storage.update(before, file, current) ? structuredClone({ operations, temporaryThreads }) : undefined;
2835
+ },
2836
+ /** Ends protection without cleanup: the old Thread is kept, and no source was ever dispatched. */
2837
+ retireAbandoned(expected, abandonedUpdateIds, authority) {
2838
+ const abandoned = [...new Set(abandonedUpdateIds)].sort((left, right) => left - right);
2839
+ return mutate(authority, (file, executor, operator) => {
2840
+ const index = file.operations.findIndex(value => value.request.operationId === expected.request.operationId);
2841
+ const operation = file.operations[index];
2842
+ if (!operation || !isDeepStrictEqual(operation, expected) || operation.operatorUserId !== operator ||
2843
+ !isDeepStrictEqual(operation.executor, executor) || operation.phase !== "ready" || operation.routing !== undefined ||
2844
+ !isDeepStrictEqual(abandoned, operation.request.source.updateIds))
2845
+ return undefined;
2846
+ file.operations.splice(index, 1);
2847
+ return operation;
2848
+ });
2849
+ },
2850
+ /** Caller proves current canonical ownership and authenticates a read-only observation, including successors. */
2851
+ confirmInspectedReady(expected, observed, authority) {
2852
+ return advance(expected, authority, operation => {
2853
+ if ((operation.phase !== "recipient-issued" && operation.phase !== "ready") || !isTelegramWorkspaceRestoreRecipient(observed) ||
2854
+ observed.sessionId !== operation.request.binding.sessionId)
2855
+ return false;
2856
+ operation.phase = "ready";
2857
+ operation.readyRecipient = structuredClone(observed);
2858
+ return true;
2859
+ });
2860
+ },
2861
+ /** Caller authenticates the exact-generation ACK; stored metadata alone is never readiness proof. */
2862
+ confirmReady(expected, acknowledged, authority) {
2863
+ return advance(expected, authority, operation => {
2864
+ if (operation.phase !== "recipient-issued" || !isDeepStrictEqual(operation.recipient, acknowledged))
2865
+ return false;
2866
+ operation.phase = "ready";
2867
+ return true;
2868
+ });
2869
+ },
2870
+ };
2871
+ },
1517
2872
  list() {
1518
2873
  return Array.from(records.values()).map(cloneRecord);
1519
2874
  },
@@ -1550,6 +2905,10 @@ export function createTelegramTopicTargetStore(options) {
1550
2905
  }));
1551
2906
  },
1552
2907
  reserveThread(reservation) {
2908
+ if (workspaceRestore?.operations.some(({ request }) => conflictsWithWorkspaceRestoreProvision(request, reservation)) ||
2909
+ conflictsWithTemporaryThread(workspaceRestore, reservation.target)) {
2910
+ throw new Error("Protected Workspace Restore provisioning conflict.");
2911
+ }
1553
2912
  const next = { ...reservation };
1554
2913
  reservations = reservations.filter((existing) => existing.slot !== next.slot &&
1555
2914
  !targetMatches(existing.target, next.target));
@@ -1557,6 +2916,10 @@ export function createTelegramTopicTargetStore(options) {
1557
2916
  markDirty();
1558
2917
  },
1559
2918
  upsertPendingProvision(provision) {
2919
+ if (workspaceRestore?.operations.some(({ request }) => conflictsWithWorkspaceRestoreProvision(request, provision)) ||
2920
+ conflictsWithTemporaryThread(workspaceRestore, provision.target)) {
2921
+ throw new Error("Protected Workspace Restore provisioning conflict.");
2922
+ }
1560
2923
  const next = {
1561
2924
  ...provision,
1562
2925
  ...(provision.target ? { target: { ...provision.target } } : {}),
@@ -1569,9 +2932,9 @@ export function createTelegramTopicTargetStore(options) {
1569
2932
  const path = getPath();
1570
2933
  const recoveryPath = getRecoveryPath(path);
1571
2934
  await mkdir(dirname(recoveryPath), { recursive: true });
1572
- withTelegramFileTransaction(`${recoveryPath}.transaction`, () => {
1573
- const recoveries = readProvisionRecoveries(path);
1574
- recoveries[provision.id] = {
2935
+ withStoreTransaction(path, () => {
2936
+ const recoveries = readProvisionRecoveries(path, true);
2937
+ const recovery = {
1575
2938
  instanceId: provision.instanceId,
1576
2939
  ...(provision.profileKey ? { profileKey: provision.profileKey } : {}),
1577
2940
  ...(provision.leaderEpoch !== undefined
@@ -1579,6 +2942,12 @@ export function createTelegramTopicTargetStore(options) {
1579
2942
  : {}),
1580
2943
  target: { ...target },
1581
2944
  };
2945
+ if (Object.hasOwn(recoveries, provision.id)) {
2946
+ if (!isDeepStrictEqual(recoveries[provision.id], recovery))
2947
+ throw new Error("Conflicting Workspace provisioning recovery evidence.");
2948
+ return;
2949
+ }
2950
+ recoveries[provision.id] = recovery;
1582
2951
  const tempPath = `${recoveryPath}.${process.pid}.${randomUUID()}.tmp`;
1583
2952
  writeFileSync(tempPath, `${JSON.stringify(recoveries, null, 2)}\n`, {
1584
2953
  encoding: "utf8",
@@ -1627,10 +2996,41 @@ export function createTelegramTopicTargetStore(options) {
1627
2996
  markDirty();
1628
2997
  },
1629
2998
  setStatusSnapshot(snapshot) {
1630
- if (!loadedPath)
2999
+ if (!loadedPath) {
1631
3000
  loadedPath = getPath();
3001
+ loadedProfile = activeProfile();
3002
+ }
1632
3003
  statusSnapshot = { ...snapshot };
1633
- statusRevision += 1;
3004
+ },
3005
+ persistStatus() {
3006
+ if (runtimeProjection)
3007
+ return runtimeProjection.persist({ runtime: statusSnapshot.runtime ?? {},
3008
+ liveRoster: statusSnapshot.liveRoster ?? {}, diagnostics: statusSnapshot.diagnostics ?? {} }).then(() => undefined);
3009
+ const write = statusQueue.then(async () => {
3010
+ if (options.canPersist && !options.canPersist())
3011
+ return;
3012
+ const body = { version: 1, source: "snapshot", ...statusSnapshot };
3013
+ // Normalize to wire JSON; object key order and omitted undefined are not changes.
3014
+ const semantic = JSON.parse(JSON.stringify(body));
3015
+ const statusPath = getStatusPath();
3016
+ if (isDeepStrictEqual(semantic, lastStatusSemantic) && statusPath === lastStatusPath)
3017
+ return;
3018
+ await mkdir(dirname(statusPath), { recursive: true });
3019
+ const tempPath = `${statusPath}.${process.pid}.${Date.now()}.${randomUUID()}.tmp`;
3020
+ await writeFile(tempPath, `${JSON.stringify({ ...body, writtenAtMs: (options.getNowMs ?? Date.now)() }, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
3021
+ try {
3022
+ renameSync(tempPath, statusPath);
3023
+ chmodSync(statusPath, 0o600);
3024
+ }
3025
+ catch (error) {
3026
+ await unlink(tempPath).catch(() => undefined);
3027
+ throw error;
3028
+ }
3029
+ lastStatusSemantic = semantic;
3030
+ lastStatusPath = statusPath;
3031
+ });
3032
+ statusQueue = write.then(() => undefined, () => undefined);
3033
+ return write;
1634
3034
  },
1635
3035
  getByProfileKey(profileKey) {
1636
3036
  const ownerKey = getTelegramThreadOwnerKey(getTelegramThreadOwnerFromProfileKey(profileKey));
@@ -1701,7 +3101,7 @@ export function createTelegramTopicTargetStore(options) {
1701
3101
  listWorkspaceRetirementIntents() {
1702
3102
  return workspaceRetirements.map(cloneWorkspaceRetirementIntent);
1703
3103
  },
1704
- commitWorkspaceJournalEvidence(expected, journalBindingKeys, complete) {
3104
+ commitWorkspaceJournalEvidence(expected, journalBindingKeys, complete, journalSources) {
1705
3105
  if (workspaceRetirementCommitInFlight || hasWorkspaceRetirementConflict(expected)) {
1706
3106
  return undefined;
1707
3107
  }
@@ -1711,15 +3111,23 @@ export function createTelegramTopicTargetStore(options) {
1711
3111
  !journalBindingKeys.every((bindingKey) => typeof bindingKey === "string" && bindingKey.length > 0 && bindingKey.length <= 512))
1712
3112
  return undefined;
1713
3113
  const keys = Array.from(new Set(journalBindingKeys));
3114
+ const sources = journalSources === undefined ? current.journalSources ?? [] : normalizeWorkspaceJournalSources(journalSources);
3115
+ if (!sources || sources.some(source => !(current.journalSources ?? []).some(retained => isDeepStrictEqual(retained, source))))
3116
+ return undefined;
1714
3117
  if ((current.journalBindingsComplete === true) === complete &&
1715
- isDeepStrictEqual(current.journalBindingKeys ?? [], keys)) {
3118
+ isDeepStrictEqual(current.journalBindingKeys ?? [], keys) && isDeepStrictEqual(current.journalSources ?? [], sources)) {
1716
3119
  return cloneWorkspaceBinding(current);
1717
3120
  }
1718
3121
  const next = { ...current, updatedAtMs: getNowMs() };
1719
- if (keys.length)
3122
+ // A complete empty set stays explicit so persisted completeness survives a lossless reload.
3123
+ if (keys.length || complete)
1720
3124
  next.journalBindingKeys = keys;
1721
3125
  else
1722
3126
  delete next.journalBindingKeys;
3127
+ if (sources.length)
3128
+ next.journalSources = sources.map(source => ({ ...source }));
3129
+ else
3130
+ delete next.journalSources;
1723
3131
  if (complete)
1724
3132
  next.journalBindingsComplete = true;
1725
3133
  else
@@ -1728,6 +3136,15 @@ export function createTelegramTopicTargetStore(options) {
1728
3136
  markDirty();
1729
3137
  return cloneWorkspaceBinding(next);
1730
3138
  },
3139
+ async persistWorkspaceJournalEvidence(expected, isCurrent) {
3140
+ const path = getPath();
3141
+ const frameCurrent = () => getPath() === path && isCurrent() && isDeepStrictEqual(workspaceBindings.get(getWorkspaceBindingMapKey(expected)), expected);
3142
+ if (!frameCurrent())
3143
+ return false;
3144
+ const current = () => frameCurrent() && options.canPersist?.() !== false;
3145
+ const published = await persistSnapshot(undefined, current);
3146
+ return published && current();
3147
+ },
1731
3148
  upsertWorkspaceRetirementIntent(intent) {
1732
3149
  const next = normalizeWorkspaceRetirementIntent(intent);
1733
3150
  if (!next)
@@ -2314,6 +3731,18 @@ export function createTelegramTopicTargetStore(options) {
2314
3731
  let claim = workspaceClaims.get(nextMapKey);
2315
3732
  const replacedTargetBinding = Array.from(workspaceBindings.values()).find((existing) => existing.bindingKey !== next.bindingKey &&
2316
3733
  targetMatches(existing.target, next.target));
3734
+ const retainedSources = [
3735
+ ...(workspaceBindings.get(nextMapKey)?.journalSources ?? []),
3736
+ ...(replacedTargetBinding?.journalSources ?? []),
3737
+ ...(next.journalSources ?? []),
3738
+ ];
3739
+ if (retainedSources.length) {
3740
+ const uniqueSources = Array.from(new Map(retainedSources.map(source => [JSON.stringify(source), source])).values());
3741
+ const journalSources = normalizeWorkspaceJournalSources(uniqueSources);
3742
+ if (!journalSources)
3743
+ return undefined;
3744
+ next.journalSources = journalSources;
3745
+ }
2317
3746
  if (claimInstanceId && claim?.instanceId === claimInstanceId &&
2318
3747
  replacedTargetBinding?.slot) {
2319
3748
  claim = {
@@ -2355,7 +3784,7 @@ export function createTelegramTopicTargetStore(options) {
2355
3784
  ...(previous.journalBindingKeys ?? []),
2356
3785
  ...(next.journalBindingKeys ?? []),
2357
3786
  ]));
2358
- if (journalBindingKeys.length)
3787
+ if (journalBindingKeys.length || previous.journalBindingsComplete)
2359
3788
  next.journalBindingKeys = journalBindingKeys;
2360
3789
  else
2361
3790
  delete next.journalBindingKeys;
@@ -2665,49 +4094,7 @@ function isTelegramTopicTargetSlotOccupied(slot, records, reservations = [], pen
2665
4094
  }
2666
4095
  return false;
2667
4096
  }
2668
- export function normalizeTelegramTopicTargetThreadName(threadName) {
2669
- return threadName.replace(/\s+/g, " ").trim().slice(0, 96);
2670
- }
2671
- function getGraphemeSegments(value) {
2672
- const segmenter = Intl.Segmenter;
2673
- if (!segmenter)
2674
- return Array.from(value);
2675
- return Array.from(new segmenter(undefined, { granularity: "grapheme" }).segment(value), (part) => part.segment);
2676
- }
2677
- export function getTelegramTopicIdentityName(threadName) {
2678
- return getGraphemeSegments(normalizeTelegramTopicTargetThreadName(threadName))
2679
- .join("")
2680
- .trim();
2681
- }
2682
- const TELEGRAM_THREAD_NAME_PALETTE = {
2683
- A: ["Atlas", "Aster", "Aurora", "Anchor", "Ashen"],
2684
- B: ["Beacon", "Briar", "Boreal", "Birch", "Bison"],
2685
- C: ["Cedar", "Comet", "Cipher", "Coral", "Cinder"],
2686
- D: ["Delta", "Dawn", "Drift", "Dune", "Dagger"],
2687
- E: ["Ember", "Echo", "Eagle", "Eden", "Elder"],
2688
- F: ["Falcon", "Fjord", "Flint", "Forest", "Fable"],
2689
- G: ["Grove", "Glade", "Glyph", "Garnet", "Gale"],
2690
- H: ["Harbor", "Hawk", "Hazel", "Helix", "Haven"],
2691
- I: ["Iris", "Ivory", "Iron", "Isle", "Idea"],
2692
- J: ["Jade", "Juno", "Jolt", "Jewel", "Jasper"],
2693
- K: ["Kite", "Karma", "Kernel", "Kodiak", "Kelp"],
2694
- L: ["Lumen", "Laurel", "Lynx", "Lotus", "Lagoon"],
2695
- M: ["Maple", "Meteor", "Meadow", "Marble", "Moss"],
2696
- N: ["Nimbus", "Nova", "Nectar", "North", "Noble"],
2697
- O: ["Orion", "Onyx", "Opal", "Orbit", "Olive"],
2698
- P: ["Pine", "Pulse", "Praxis", "Pebble", "Prism"],
2699
- Q: ["Quartz", "Quill", "Quasar", "Quest", "Quiver"],
2700
- R: ["River", "Raven", "Rune", "Reef", "Ridge"],
2701
- S: ["Spruce", "Solar", "Signal", "Stone", "Sable"],
2702
- T: ["Timber", "Talon", "Terra", "Torch", "Tide"],
2703
- U: ["Umber", "Unity", "Ursa", "Uplink", "Ulmus"],
2704
- V: ["Violet", "Vector", "Vista", "Vale", "Vortex"],
2705
- W: ["Willow", "Warden", "Wave", "Winter", "Wisp"],
2706
- X: ["Xenon", "Xylem", "Xavier", "Xylo", "Xerus"],
2707
- Y: ["Yarrow", "Yonder", "Yukon", "Yale", "Yogi"],
2708
- Z: ["Zenith", "Zephyr", "Zircon", "Zebra", "Zion"],
2709
- };
2710
- export function listOccupiedTelegramThreadIdentities(input) {
4097
+ function listOccupiedTelegramThreadIdentities(input) {
2711
4098
  const occupied = new Set();
2712
4099
  const add = (threadName) => {
2713
4100
  if (!threadName)
@@ -2740,35 +4127,6 @@ export function listOccupiedTelegramThreadIdentities(input) {
2740
4127
  }
2741
4128
  return Array.from(occupied);
2742
4129
  }
2743
- export function chooseTelegramThreadName(input) {
2744
- if (!input.slot || !/^[A-Z]$/.test(input.slot))
2745
- return undefined;
2746
- const names = TELEGRAM_THREAD_NAME_PALETTE[input.slot];
2747
- if (!names || names.length === 0)
2748
- return undefined;
2749
- const occupied = new Set((input.occupied ?? []).map((name) => getTelegramTopicIdentityName(name)));
2750
- const start = input.getRandom
2751
- ? Math.max(0, Math.min(names.length - 1, Math.floor(input.getRandom() * names.length)))
2752
- : getTelegramThreadNameEntropyIndex(input.entropy, names.length);
2753
- for (let offset = 0; offset < names.length; offset += 1) {
2754
- const name = names[(start + offset) % names.length];
2755
- if (!occupied.has(getTelegramTopicIdentityName(name)))
2756
- return name;
2757
- }
2758
- for (const paletteSlot of "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
2759
- for (const name of TELEGRAM_THREAD_NAME_PALETTE[paletteSlot] ?? []) {
2760
- if (!occupied.has(getTelegramTopicIdentityName(name)))
2761
- return name;
2762
- }
2763
- }
2764
- return undefined;
2765
- }
2766
- function getTelegramThreadNameLeadingSlot(threadName) {
2767
- if (!threadName)
2768
- return undefined;
2769
- const first = getTelegramTopicIdentityName(threadName)[0];
2770
- return first && /^[A-Z]$/.test(first) ? first : undefined;
2771
- }
2772
4130
  function getNextTelegramThreadNamePaletteSlot(records, fallbackSlot) {
2773
4131
  let maxCode = "A".charCodeAt(0) - 1;
2774
4132
  for (const record of records) {
@@ -2787,79 +4145,6 @@ function getNextTelegramThreadNamePaletteSlot(records, fallbackSlot) {
2787
4145
  code = "A".charCodeAt(0);
2788
4146
  return String.fromCharCode(code);
2789
4147
  }
2790
- function getTelegramThreadNameEntropyIndex(entropy, length) {
2791
- if (length <= 1)
2792
- return 0;
2793
- if (typeof entropy === "number" && entropy < 1_000_000_000_000)
2794
- return 0;
2795
- const value = entropy === undefined ? "0" : String(entropy);
2796
- let hash = 2166136261;
2797
- for (const char of value) {
2798
- hash ^= char.codePointAt(0) ?? 0;
2799
- hash = Math.imul(hash, 16777619) >>> 0;
2800
- }
2801
- return hash % length;
2802
- }
2803
- export function getTelegramTopicThreadNameValidationError(threadName, _slot) {
2804
- const identity = getTelegramTopicIdentityName(threadName);
2805
- const reasons = [];
2806
- if (!identity)
2807
- reasons.push("it is empty after trimming");
2808
- if (/\s/.test(identity))
2809
- reasons.push("it contains spaces");
2810
- if (/[^A-Za-z]/.test(identity)) {
2811
- reasons.push("it contains characters outside Latin A-Z letters");
2812
- }
2813
- if (!/^[A-Z]/.test(identity)) {
2814
- reasons.push("it does not start with an uppercase Latin letter");
2815
- }
2816
- const genericLabels = new Set(["telegram", "leader", "follower"]);
2817
- if (genericLabels.has(identity.toLowerCase())) {
2818
- reasons.push("it is a generic role label");
2819
- }
2820
- if (/^[A-Z]$/.test(identity))
2821
- reasons.push("it is only a bare slot letter");
2822
- if (reasons.length === 0)
2823
- return undefined;
2824
- return `Invalid Telegram instance name: ${reasons.join("; ")}. Use exactly one capitalized Latin word with no spaces, punctuation, emoji, non-Latin letters, or digits; it must not be a generic role label or only a bare slot letter.`;
2825
- }
2826
- export function getTelegramManualThreadDisplayNameValidationError(threadName) {
2827
- const trimmed = threadName.trim();
2828
- const normalized = trimmed.replace(/\s+/g, " ");
2829
- const reasons = [];
2830
- if (!trimmed)
2831
- reasons.push("it is empty after trimming");
2832
- if (trimmed && /[^\x20-\x7E]/.test(trimmed)) {
2833
- reasons.push("it contains characters outside printable ASCII");
2834
- }
2835
- if (normalized.length > 96)
2836
- reasons.push("it is longer than 96 characters");
2837
- if (/^[A-Z]$/.test(normalized)) {
2838
- reasons.push("a bare slot letter is reserved for reset to automatic");
2839
- }
2840
- if (reasons.length === 0)
2841
- return undefined;
2842
- return `Invalid Telegram Thread display name: ${reasons.join("; ")}. Use 1–96 printable ASCII characters.`;
2843
- }
2844
- export function isTelegramTopicThreadNameValidForSlot(threadName, slot) {
2845
- return !getTelegramTopicThreadNameValidationError(threadName, slot);
2846
- }
2847
- function applyTopicNameTemplate(template, request, slot) {
2848
- const threadName = request.threadName?.replace(/\s+/g, " ").trim() || request.profileKey;
2849
- let result = template
2850
- .replaceAll("{threadName}", threadName)
2851
- .replaceAll("{profileKey}", request.profileKey)
2852
- .replaceAll("{instanceId}", request.instanceId);
2853
- if (slot)
2854
- result = result.replaceAll("{slot}", slot);
2855
- return result;
2856
- }
2857
- export function getTelegramTopicName(request, template = "{slot}", slot) {
2858
- const name = applyTopicNameTemplate(template, request, slot)
2859
- .replace(/\s+/g, " ")
2860
- .trim();
2861
- return (name || slot || "Pi").slice(0, 128);
2862
- }
2863
4148
  function asInteger(value) {
2864
4149
  if (typeof value === "number" && Number.isInteger(value))
2865
4150
  return value;
@@ -3544,13 +4829,6 @@ export function isTelegramTopicModeUnavailableError(error) {
3544
4829
  message.includes("threaded mode") ||
3545
4830
  message.includes("method is available only for"));
3546
4831
  }
3547
- export function getTelegramTopicTitleForThreadName(threadName, slot, template = "{threadName}") {
3548
- return getTelegramTopicName({
3549
- instanceId: "",
3550
- profileKey: normalizeTelegramTopicTargetThreadName(threadName) || "Pi",
3551
- threadName,
3552
- }, template, slot);
3553
- }
3554
4832
  export function createTelegramTopicTargetRenamer(deps) {
3555
4833
  return async (request) => {
3556
4834
  const threadName = normalizeTelegramTopicTargetThreadName(request.threadName);
@@ -3764,7 +5042,7 @@ export function createTelegramTopicTargetProvisioner(deps) {
3764
5042
  if (!projectedTitle?.trim()) {
3765
5043
  throw new Error("Telegram Thread display identity is missing or ambiguous.");
3766
5044
  }
3767
- displayTitle = getTelegramTopicName({ ...request, threadName: projectedTitle }, "{threadName}", slot);
5045
+ displayTitle = buildTelegramTopicName({ ...request, threadName: projectedTitle }, "{threadName}", slot);
3768
5046
  }
3769
5047
  const pendingId = `provision:${request.instanceId}:${slot}:${nowMs}`;
3770
5048
  const pendingOwner = request.owner?.kind === "leader" ? "leader" : "manual-follower";
@@ -3789,7 +5067,7 @@ export function createTelegramTopicTargetProvisioner(deps) {
3789
5067
  assertLeaderEpoch("before-createForumTopic");
3790
5068
  const topic = await deps.callApi("createForumTopic", {
3791
5069
  chat_id: deps.topicChatId,
3792
- name: displayTitle ?? getTelegramTopicName({
5070
+ name: displayTitle ?? buildTelegramTopicName({
3793
5071
  ...request,
3794
5072
  ...(requestThreadName ? { threadName: requestThreadName } : {}),
3795
5073
  }, deps.topicNameTemplate ??