@llblab/pi-telegram 0.52.2 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +65 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +252 -127
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +128 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +516 -301
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
@@ -3,30 +3,31 @@
3
3
  * Zones: telegram threads, workspace lifecycle
4
4
  * Owns fail-closed cleanup eligibility projection without persistence or Bot API effects
5
5
  */
6
- import { chmodSync, closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
7
- import { dirname } from "node:path";
8
6
  import { createHash, randomUUID } from "node:crypto";
9
- import { renameTelegramPathWithRetry, withTelegramFileTransaction } from "./locks.js";
7
+ import { publishTelegramPrivateFile, readTelegramPrivateFile, TelegramPrivateFileError, withTelegramFileTransaction, } from "./locks.js";
10
8
  import { getTelegramJournalPublicationPaths } from "./paths.js";
11
9
  import { isWireRecord as isObject, hasOnlyWireKeys as onlyKeys, isNonNegativeWireInteger as safeTime, } from "./wire.js";
12
10
  function targetKey(target) {
13
11
  return `${target.chatId}:${target.threadId}`;
14
12
  }
15
13
  function validTarget(target) {
16
- return Number.isSafeInteger(target.chatId) && Number.isSafeInteger(target.threadId) &&
17
- target.threadId > 0;
14
+ return (Number.isSafeInteger(target.chatId) &&
15
+ Number.isSafeInteger(target.threadId) &&
16
+ target.threadId > 0);
18
17
  }
19
18
  function validSessionIdentity(value) {
20
19
  if (value.sessionId === undefined && value.sessionKey === undefined)
21
20
  return true;
22
- if (typeof value.sessionId !== "string" || !value.sessionId ||
21
+ if (typeof value.sessionId !== "string" ||
22
+ !value.sessionId ||
23
23
  value.sessionId !== value.sessionId.trim() ||
24
24
  Buffer.byteLength(value.sessionId, "utf8") > 256 ||
25
- typeof value.sessionKey !== "string" || !/^[a-f0-9]{64}$/u.test(value.sessionKey)) {
25
+ typeof value.sessionKey !== "string" ||
26
+ !/^[a-f0-9]{64}$/u.test(value.sessionKey)) {
26
27
  return false;
27
28
  }
28
- return createHash("sha256").update(value.sessionId).digest("hex") ===
29
- value.sessionKey;
29
+ return (createHash("sha256").update(value.sessionId).digest("hex") ===
30
+ value.sessionKey);
30
31
  }
31
32
  /** Returns no candidates when any identity/evidence ambiguity exists. */
32
33
  export function planTelegramInactiveThreadCleanup(input) {
@@ -37,16 +38,25 @@ export function planTelegramInactiveThreadCleanup(input) {
37
38
  for (const binding of input.bindings) {
38
39
  const key = targetKey(binding.target);
39
40
  const slot = binding.slot;
40
- if (!binding.bindingKey || !binding.cwd || !binding.workspaceKey || !binding.instanceSlot ||
41
- typeof slot !== "string" || !slot || !validSessionIdentity(binding) ||
42
- !validTarget(binding.target) || bindingKeys.has(binding.bindingKey) || bindingTargets.has(key))
41
+ if (!binding.bindingKey ||
42
+ !binding.cwd ||
43
+ !binding.workspaceKey ||
44
+ !binding.instanceSlot ||
45
+ typeof slot !== "string" ||
46
+ !slot ||
47
+ !validSessionIdentity(binding) ||
48
+ !validTarget(binding.target) ||
49
+ bindingKeys.has(binding.bindingKey) ||
50
+ bindingTargets.has(key))
43
51
  return [];
44
52
  bindingKeys.add(binding.bindingKey);
45
53
  bindingTargets.add(key);
46
54
  }
47
55
  const evidenceByBinding = new Map();
48
56
  for (const evidence of input.protection) {
49
- if (!evidence.bindingKey || evidenceByBinding.has(evidence.bindingKey) || !validTarget(evidence.target))
57
+ if (!evidence.bindingKey ||
58
+ evidenceByBinding.has(evidence.bindingKey) ||
59
+ !validTarget(evidence.target))
50
60
  return [];
51
61
  evidenceByBinding.set(evidence.bindingKey, evidence);
52
62
  }
@@ -55,80 +65,120 @@ export function planTelegramInactiveThreadCleanup(input) {
55
65
  ...(input.provisioningTargets ?? []),
56
66
  ...(input.cleanupTargets ?? []),
57
67
  ];
58
- if (competingTargets.some(target => !validTarget(target)))
68
+ if (competingTargets.some((target) => !validTarget(target)))
59
69
  return [];
60
70
  const competing = new Set(competingTargets.map(targetKey));
61
71
  const candidates = [];
62
72
  for (const binding of input.bindings) {
63
73
  const inactiveSinceMs = binding.inactiveSinceMs;
64
74
  const slot = binding.slot;
65
- if (typeof slot !== "string" || !slot || !Number.isSafeInteger(inactiveSinceMs) ||
66
- inactiveSinceMs === undefined || inactiveSinceMs < 0 ||
67
- !Number.isSafeInteger(binding.updatedAtMs) || binding.updatedAtMs < inactiveSinceMs)
75
+ if (typeof slot !== "string" ||
76
+ !slot ||
77
+ !Number.isSafeInteger(inactiveSinceMs) ||
78
+ inactiveSinceMs === undefined ||
79
+ inactiveSinceMs < 0 ||
80
+ !Number.isSafeInteger(binding.updatedAtMs) ||
81
+ binding.updatedAtMs < inactiveSinceMs)
68
82
  continue;
69
83
  const evidence = evidenceByBinding.get(binding.bindingKey);
70
- if (!evidence || targetKey(evidence.target) !== targetKey(binding.target) ||
71
- evidence.liveOwner !== "clear" || evidence.acceptedWork !== "clear" ||
72
- evidence.deliveryAuthority !== "clear" || competing.has(targetKey(binding.target)))
84
+ if (!evidence ||
85
+ targetKey(evidence.target) !== targetKey(binding.target) ||
86
+ evidence.liveOwner !== "clear" ||
87
+ evidence.acceptedWork !== "clear" ||
88
+ evidence.deliveryAuthority !== "clear" ||
89
+ competing.has(targetKey(binding.target)))
73
90
  continue;
74
- candidates.push({ profileName: input.profileName, bindingKey: binding.bindingKey,
75
- cwd: binding.cwd, workspaceKey: binding.workspaceKey,
91
+ candidates.push({
92
+ profileName: input.profileName,
93
+ bindingKey: binding.bindingKey,
94
+ cwd: binding.cwd,
95
+ workspaceKey: binding.workspaceKey,
76
96
  ...(binding.sessionId && binding.sessionKey
77
97
  ? { sessionId: binding.sessionId, sessionKey: binding.sessionKey }
78
98
  : {}),
79
- instanceSlot: binding.instanceSlot, slot, target: { ...binding.target }, inactiveSinceMs,
80
- bindingUpdatedAtMs: binding.updatedAtMs });
99
+ instanceSlot: binding.instanceSlot,
100
+ slot,
101
+ target: { ...binding.target },
102
+ inactiveSinceMs,
103
+ bindingUpdatedAtMs: binding.updatedAtMs,
104
+ });
81
105
  }
82
106
  return candidates.sort((left, right) => left.bindingKey.localeCompare(right.bindingKey));
83
107
  }
84
108
  export function captureTelegramInactiveThreadCleanupEvidence(input) {
85
109
  const sourceBindings = input.listBindings();
86
- const bindings = sourceBindings.map(binding => ({
87
- cwd: binding.cwd, workspaceKey: binding.workspaceKey,
110
+ const bindings = sourceBindings.map((binding) => ({
111
+ cwd: binding.cwd,
112
+ workspaceKey: binding.workspaceKey,
88
113
  ...(binding.sessionId && binding.sessionKey
89
114
  ? { sessionId: binding.sessionId, sessionKey: binding.sessionKey }
90
115
  : {}),
91
116
  instanceSlot: binding.instanceSlot,
92
- ...(binding.slot === undefined ? {} : { slot: binding.slot }), bindingKey: binding.bindingKey, target: { ...binding.target },
93
- ...(binding.inactiveSinceMs === undefined ? {} : { inactiveSinceMs: binding.inactiveSinceMs }),
117
+ ...(binding.slot === undefined ? {} : { slot: binding.slot }),
118
+ bindingKey: binding.bindingKey,
119
+ target: { ...binding.target },
120
+ ...(binding.inactiveSinceMs === undefined
121
+ ? {}
122
+ : { inactiveSinceMs: binding.inactiveSinceMs }),
94
123
  updatedAtMs: binding.updatedAtMs,
95
124
  }));
96
125
  const protection = sourceBindings.map((source, index) => {
97
126
  const binding = bindings[index];
98
127
  try {
99
- return { bindingKey: binding.bindingKey, target: { ...binding.target },
100
- ...input.getProtection(source) };
128
+ return {
129
+ bindingKey: binding.bindingKey,
130
+ target: { ...binding.target },
131
+ ...input.getProtection(source),
132
+ };
101
133
  }
102
134
  catch {
103
- return { bindingKey: binding.bindingKey, target: { ...binding.target },
104
- liveOwner: "unknown", acceptedWork: "unknown",
105
- deliveryAuthority: "unknown" };
135
+ return {
136
+ bindingKey: binding.bindingKey,
137
+ target: { ...binding.target },
138
+ liveOwner: "unknown",
139
+ acceptedWork: "unknown",
140
+ deliveryAuthority: "unknown",
141
+ };
106
142
  }
107
143
  });
108
144
  return {
109
145
  profileName: input.profileName,
110
146
  bindings,
111
147
  protection,
112
- reservedTargets: input.listReservations().map(entry => ({ ...entry.target })),
113
- provisioningTargets: input.listPendingProvisions().flatMap(entry => entry.target ? [{ ...entry.target }] : []),
114
- cleanupTargets: input.listPendingCleanups().map(entry => ({ ...entry.target })),
148
+ reservedTargets: input
149
+ .listReservations()
150
+ .map((entry) => ({ ...entry.target })),
151
+ provisioningTargets: input
152
+ .listPendingProvisions()
153
+ .flatMap((entry) => (entry.target ? [{ ...entry.target }] : [])),
154
+ cleanupTargets: input
155
+ .listPendingCleanups()
156
+ .map((entry) => ({ ...entry.target })),
115
157
  };
116
158
  }
117
159
  export function createTelegramInactiveThreadCleanupReviewRuntime(deps) {
118
160
  return {
119
161
  review() {
120
- return deps.runWorkspaceOperation({ operationId: `thread-cleanup-review:${randomUUID()}`,
121
- operationKind: "thread-cleanup-review", scopes: [{ kind: "profile" }] }, async () => {
162
+ return deps.runWorkspaceOperation({
163
+ operationId: `thread-cleanup-review:${randomUUID()}`,
164
+ operationKind: "thread-cleanup-review",
165
+ scopes: [{ kind: "profile" }],
166
+ }, async () => {
122
167
  const evidence = captureTelegramInactiveThreadCleanupEvidence({
123
- profileName: deps.getProfileName(), listBindings: deps.listBindings,
124
- getProtection: deps.getProtection, listReservations: deps.listReservations,
168
+ profileName: deps.getProfileName(),
169
+ listBindings: deps.listBindings,
170
+ getProtection: deps.getProtection,
171
+ listReservations: deps.listReservations,
125
172
  listPendingProvisions: deps.listPendingProvisions,
126
173
  listPendingCleanups: deps.listPendingCleanups,
127
174
  });
128
175
  const candidates = planTelegramInactiveThreadCleanup(evidence);
129
176
  if (candidates.length === 0)
130
177
  return { count: 0 };
131
- const digest = createHash("sha256").update(JSON.stringify(candidates)).digest("hex").slice(0, 32);
178
+ const digest = createHash("sha256")
179
+ .update(JSON.stringify(candidates))
180
+ .digest("hex")
181
+ .slice(0, 32);
132
182
  const operationId = `thread-cleanup:${digest}`;
133
183
  deps.getWorkStore().prepare(operationId, candidates);
134
184
  return { count: candidates.length, operationId };
@@ -137,54 +187,126 @@ export function createTelegramInactiveThreadCleanupReviewRuntime(deps) {
137
187
  };
138
188
  }
139
189
  function validateWorkSet(value, profileName) {
140
- if (!isObject(value) || !onlyKeys(value, ["operationId", "createdAtMs", "entries"]) ||
141
- typeof value.operationId !== "string" || !value.operationId || !safeTime(value.createdAtMs) ||
142
- !Array.isArray(value.entries) || value.entries.length === 0)
190
+ if (!isObject(value) ||
191
+ !onlyKeys(value, ["operationId", "createdAtMs", "entries"]) ||
192
+ typeof value.operationId !== "string" ||
193
+ !value.operationId ||
194
+ !safeTime(value.createdAtMs) ||
195
+ !Array.isArray(value.entries) ||
196
+ value.entries.length === 0)
143
197
  throw new Error("Telegram Thread cleanup work-set schema is invalid.");
144
198
  const createdAtMs = value.createdAtMs;
145
- const entries = value.entries.map(raw => {
146
- if (!isObject(raw) || !onlyKeys(raw, ["profileName", "bindingKey", "cwd", "workspaceKey",
147
- "sessionId", "sessionKey", "instanceSlot", "slot", "target", "inactiveSinceMs", "bindingUpdatedAtMs", "state", "updatedAtMs",
148
- "issuedAtMs", "permitOperationId", "permitIntentId", "permitLeaderEpoch", "deletedAtMs"]) || raw.profileName !== profileName || typeof raw.bindingKey !== "string" ||
149
- !raw.bindingKey || typeof raw.cwd !== "string" || !raw.cwd || typeof raw.workspaceKey !== "string" ||
150
- !raw.workspaceKey || !validSessionIdentity(raw) ||
151
- typeof raw.instanceSlot !== "string" || !raw.instanceSlot ||
152
- typeof raw.slot !== "string" || !raw.slot || !isObject(raw.target) ||
153
- !onlyKeys(raw.target, ["chatId", "threadId"]) || !validTarget(raw.target) ||
154
- !safeTime(raw.inactiveSinceMs) || !safeTime(raw.bindingUpdatedAtMs) ||
155
- raw.bindingUpdatedAtMs < raw.inactiveSinceMs || !safeTime(raw.updatedAtMs))
199
+ const entries = value.entries.map((raw) => {
200
+ if (!isObject(raw) ||
201
+ !onlyKeys(raw, [
202
+ "profileName",
203
+ "bindingKey",
204
+ "cwd",
205
+ "workspaceKey",
206
+ "sessionId",
207
+ "sessionKey",
208
+ "instanceSlot",
209
+ "slot",
210
+ "target",
211
+ "inactiveSinceMs",
212
+ "bindingUpdatedAtMs",
213
+ "state",
214
+ "updatedAtMs",
215
+ "issuedAtMs",
216
+ "permitOperationId",
217
+ "permitIntentId",
218
+ "permitLeaderEpoch",
219
+ "deletedAtMs",
220
+ ]) ||
221
+ raw.profileName !== profileName ||
222
+ typeof raw.bindingKey !== "string" ||
223
+ !raw.bindingKey ||
224
+ typeof raw.cwd !== "string" ||
225
+ !raw.cwd ||
226
+ typeof raw.workspaceKey !== "string" ||
227
+ !raw.workspaceKey ||
228
+ !validSessionIdentity(raw) ||
229
+ typeof raw.instanceSlot !== "string" ||
230
+ !raw.instanceSlot ||
231
+ typeof raw.slot !== "string" ||
232
+ !raw.slot ||
233
+ !isObject(raw.target) ||
234
+ !onlyKeys(raw.target, ["chatId", "threadId"]) ||
235
+ !validTarget(raw.target) ||
236
+ !safeTime(raw.inactiveSinceMs) ||
237
+ !safeTime(raw.bindingUpdatedAtMs) ||
238
+ raw.bindingUpdatedAtMs < raw.inactiveSinceMs ||
239
+ !safeTime(raw.updatedAtMs))
156
240
  throw new Error("Telegram Thread cleanup entry schema is invalid.");
157
- const base = { profileName, bindingKey: raw.bindingKey, cwd: raw.cwd, workspaceKey: raw.workspaceKey,
158
- ...(typeof raw.sessionId === "string" && typeof raw.sessionKey === "string"
241
+ const base = {
242
+ profileName,
243
+ bindingKey: raw.bindingKey,
244
+ cwd: raw.cwd,
245
+ workspaceKey: raw.workspaceKey,
246
+ ...(typeof raw.sessionId === "string" &&
247
+ typeof raw.sessionKey === "string"
159
248
  ? { sessionId: raw.sessionId, sessionKey: raw.sessionKey }
160
249
  : {}),
161
- instanceSlot: raw.instanceSlot, slot: raw.slot, target: { chatId: raw.target.chatId,
162
- threadId: raw.target.threadId }, inactiveSinceMs: raw.inactiveSinceMs,
163
- bindingUpdatedAtMs: raw.bindingUpdatedAtMs, updatedAtMs: raw.updatedAtMs };
250
+ instanceSlot: raw.instanceSlot,
251
+ slot: raw.slot,
252
+ target: {
253
+ chatId: raw.target.chatId,
254
+ threadId: raw.target.threadId,
255
+ },
256
+ inactiveSinceMs: raw.inactiveSinceMs,
257
+ bindingUpdatedAtMs: raw.bindingUpdatedAtMs,
258
+ updatedAtMs: raw.updatedAtMs,
259
+ };
164
260
  if (base.updatedAtMs < base.bindingUpdatedAtMs)
165
261
  throw new Error("Telegram Thread cleanup entry clock is invalid.");
166
- const noPermit = raw.permitOperationId === undefined && raw.permitIntentId === undefined &&
262
+ const noPermit = raw.permitOperationId === undefined &&
263
+ raw.permitIntentId === undefined &&
167
264
  raw.permitLeaderEpoch === undefined;
168
- if (raw.state === "prepared" && raw.issuedAtMs === undefined && raw.deletedAtMs === undefined && noPermit)
265
+ if (raw.state === "prepared" &&
266
+ raw.issuedAtMs === undefined &&
267
+ raw.deletedAtMs === undefined &&
268
+ noPermit)
169
269
  return { ...base, state: "prepared" };
170
- const validPermit = typeof raw.permitOperationId === "string" && !!raw.permitOperationId &&
171
- typeof raw.permitIntentId === "string" && !!raw.permitIntentId &&
172
- (typeof raw.permitLeaderEpoch === "string" || Number.isSafeInteger(raw.permitLeaderEpoch));
173
- if (raw.state === "outcome-unknown" && safeTime(raw.issuedAtMs) && validPermit &&
174
- raw.issuedAtMs === base.updatedAtMs && raw.deletedAtMs === undefined)
175
- return { ...base, state: "outcome-unknown", issuedAtMs: raw.issuedAtMs,
176
- permitOperationId: raw.permitOperationId, permitIntentId: raw.permitIntentId,
177
- permitLeaderEpoch: raw.permitLeaderEpoch };
178
- if (raw.state === "deleted" && safeTime(raw.issuedAtMs) && validPermit && safeTime(raw.deletedAtMs) &&
179
- raw.deletedAtMs === base.updatedAtMs && raw.deletedAtMs >= raw.issuedAtMs)
180
- return { ...base, state: "deleted", issuedAtMs: raw.issuedAtMs,
181
- permitOperationId: raw.permitOperationId, permitIntentId: raw.permitIntentId,
182
- permitLeaderEpoch: raw.permitLeaderEpoch, deletedAtMs: raw.deletedAtMs };
270
+ const validPermit = typeof raw.permitOperationId === "string" &&
271
+ !!raw.permitOperationId &&
272
+ typeof raw.permitIntentId === "string" &&
273
+ !!raw.permitIntentId &&
274
+ (typeof raw.permitLeaderEpoch === "string" ||
275
+ Number.isSafeInteger(raw.permitLeaderEpoch));
276
+ if (raw.state === "outcome-unknown" &&
277
+ safeTime(raw.issuedAtMs) &&
278
+ validPermit &&
279
+ raw.issuedAtMs === base.updatedAtMs &&
280
+ raw.deletedAtMs === undefined)
281
+ return {
282
+ ...base,
283
+ state: "outcome-unknown",
284
+ issuedAtMs: raw.issuedAtMs,
285
+ permitOperationId: raw.permitOperationId,
286
+ permitIntentId: raw.permitIntentId,
287
+ permitLeaderEpoch: raw.permitLeaderEpoch,
288
+ };
289
+ if (raw.state === "deleted" &&
290
+ safeTime(raw.issuedAtMs) &&
291
+ validPermit &&
292
+ safeTime(raw.deletedAtMs) &&
293
+ raw.deletedAtMs === base.updatedAtMs &&
294
+ raw.deletedAtMs >= raw.issuedAtMs)
295
+ return {
296
+ ...base,
297
+ state: "deleted",
298
+ issuedAtMs: raw.issuedAtMs,
299
+ permitOperationId: raw.permitOperationId,
300
+ permitIntentId: raw.permitIntentId,
301
+ permitLeaderEpoch: raw.permitLeaderEpoch,
302
+ deletedAtMs: raw.deletedAtMs,
303
+ };
183
304
  throw new Error("Telegram Thread cleanup entry state is invalid.");
184
305
  });
185
- if (entries.some(entry => entry.updatedAtMs < createdAtMs) ||
186
- new Set(entries.map(entry => entry.bindingKey)).size !== entries.length ||
187
- new Set(entries.map(entry => targetKey(entry.target))).size !== entries.length)
306
+ if (entries.some((entry) => entry.updatedAtMs < createdAtMs) ||
307
+ new Set(entries.map((entry) => entry.bindingKey)).size !== entries.length ||
308
+ new Set(entries.map((entry) => targetKey(entry.target))).size !==
309
+ entries.length)
188
310
  throw new Error("Telegram Thread cleanup work-set identity is ambiguous.");
189
311
  return { operationId: value.operationId, createdAtMs, entries };
190
312
  }
@@ -192,11 +314,16 @@ function sameCandidates(left, right) {
192
314
  return JSON.stringify(left) === JSON.stringify(right);
193
315
  }
194
316
  function parseTelegramThreadCleanupWorkFile(value, profileName, tokenSha256) {
195
- if (!isObject(value) || !onlyKeys(value, ["version", "profileName", "tokenSha256", "workSets"]) ||
196
- value.version !== 1 || value.profileName !== profileName || value.tokenSha256 !== tokenSha256 || !Array.isArray(value.workSets))
317
+ if (!isObject(value) ||
318
+ !onlyKeys(value, ["version", "profileName", "tokenSha256", "workSets"]) ||
319
+ value.version !== 1 ||
320
+ value.profileName !== profileName ||
321
+ value.tokenSha256 !== tokenSha256 ||
322
+ !Array.isArray(value.workSets))
197
323
  throw new Error("Telegram Thread cleanup store identity or schema does not match.");
198
- const workSets = value.workSets.map(workSet => validateWorkSet(workSet, profileName));
199
- if (new Set(workSets.map(workSet => workSet.operationId)).size !== workSets.length)
324
+ const workSets = value.workSets.map((workSet) => validateWorkSet(workSet, profileName));
325
+ if (new Set(workSets.map((workSet) => workSet.operationId)).size !==
326
+ workSets.length)
200
327
  throw new Error("Telegram Thread cleanup operation identity is ambiguous.");
201
328
  return { version: 1, profileName, tokenSha256, workSets };
202
329
  }
@@ -206,40 +333,35 @@ export function createTelegramThreadCleanupWorkStore(options) {
206
333
  const maxWorkSets = options.maxWorkSets ?? 32;
207
334
  const maxBytes = options.maxBytes ?? 1024 * 1024;
208
335
  const now = options.getNowMs ?? Date.now;
209
- if (!options.path || !options.profileName || !/^[a-f0-9]{64}$/u.test(options.tokenSha256) ||
210
- !Number.isSafeInteger(maxWorkSets) || maxWorkSets <= 0 ||
211
- !Number.isSafeInteger(maxBytes) || maxBytes <= 0)
336
+ if (!options.path ||
337
+ !options.profileName ||
338
+ !/^[a-f0-9]{64}$/u.test(options.tokenSha256) ||
339
+ !Number.isSafeInteger(maxWorkSets) ||
340
+ maxWorkSets <= 0 ||
341
+ !Number.isSafeInteger(maxBytes) ||
342
+ maxBytes <= 0)
212
343
  throw new Error("Telegram Thread cleanup store options are invalid.");
213
- const empty = () => ({ version: 1, profileName: options.profileName,
214
- tokenSha256: options.tokenSha256, workSets: [] });
344
+ const empty = () => ({
345
+ version: 1,
346
+ profileName: options.profileName,
347
+ tokenSha256: options.tokenSha256,
348
+ workSets: [],
349
+ });
215
350
  const read = () => {
216
- let before;
351
+ let text;
217
352
  try {
218
- before = lstatSync(options.path, { bigint: true });
353
+ text = readTelegramPrivateFile(options.path, maxBytes);
219
354
  }
220
355
  catch (error) {
221
- if (error.code === "ENOENT")
222
- return empty();
223
- throw error;
224
- }
225
- const uid = process.getuid?.();
226
- if (!constants.O_NOFOLLOW || !constants.O_NONBLOCK || uid === undefined || !before.isFile() ||
227
- before.isSymbolicLink() || before.uid !== BigInt(uid) || before.nlink !== 1n ||
228
- (before.mode & 63n) !== 0n || before.size > BigInt(maxBytes))
229
- throw new Error("Telegram Thread cleanup store is not a bounded private regular file.");
230
- const fd = openSync(options.path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
231
- let value;
232
- try {
233
- const opened = fstatSync(fd, { bigint: true });
234
- if (opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== before.size ||
235
- opened.mtimeNs !== before.mtimeNs)
236
- throw new Error("Telegram Thread cleanup store changed during inspection.");
237
- value = JSON.parse(readFileSync(fd, "utf8"));
356
+ if (!(error instanceof TelegramPrivateFileError))
357
+ throw error;
358
+ throw new Error(error.failure === "changed"
359
+ ? "Telegram Thread cleanup store changed during inspection."
360
+ : "Telegram Thread cleanup store is not a bounded private regular file.", { cause: error });
238
361
  }
239
- finally {
240
- closeSync(fd);
241
- }
242
- return parseTelegramThreadCleanupWorkFile(value, options.profileName, options.tokenSha256);
362
+ return text === undefined
363
+ ? empty()
364
+ : parseTelegramThreadCleanupWorkFile(JSON.parse(text), options.profileName, options.tokenSha256);
243
365
  };
244
366
  const publish = (file) => {
245
367
  if (file.workSets.length > maxWorkSets)
@@ -247,32 +369,17 @@ export function createTelegramThreadCleanupWorkStore(options) {
247
369
  const serialized = `${JSON.stringify(file, null, 2)}\n`;
248
370
  if (Buffer.byteLength(serialized) > maxBytes)
249
371
  throw new Error("Telegram Thread cleanup byte capacity reached.");
250
- const temporaryPath = `${publicationPaths.temporaryBasePath}.${process.pid}.${randomUUID()}.tmp`;
251
- mkdirSync(dirname(options.path), { recursive: true, mode: 0o700 });
252
- mkdirSync(dirname(temporaryPath), { recursive: true, mode: 0o700 });
253
- try {
254
- writeFileSync(temporaryPath, serialized, { mode: 0o600 });
255
- chmodSync(temporaryPath, 0o600);
256
- options.onPublicationBoundary?.("after-write-before-rename");
257
- if (!renameTelegramPathWithRetry(temporaryPath, options.path))
258
- throw new Error("Telegram Thread cleanup staging file disappeared.");
259
- chmodSync(options.path, 0o600);
260
- options.onPublicationBoundary?.("after-rename");
261
- }
262
- finally {
263
- try {
264
- unlinkSync(temporaryPath);
265
- }
266
- catch { /* rename consumed it */ }
267
- }
372
+ publishTelegramPrivateFile(options.path, publicationPaths.temporaryBasePath, serialized, "Telegram Thread cleanup", options.onPublicationBoundary);
268
373
  };
269
374
  const mutate = (operation) => withTelegramFileTransaction(publicationPaths.transactionPath, () => operation(read()));
270
375
  return {
271
376
  prepare(operationId, candidates) {
272
- if (!operationId || candidates.length === 0 || candidates.some(candidate => candidate.profileName !== options.profileName))
377
+ if (!operationId ||
378
+ candidates.length === 0 ||
379
+ candidates.some((candidate) => candidate.profileName !== options.profileName))
273
380
  throw new Error("Telegram Thread cleanup work-set is invalid.");
274
- return mutate(file => {
275
- const existing = file.workSets.find(workSet => workSet.operationId === operationId);
381
+ return mutate((file) => {
382
+ const existing = file.workSets.find((workSet) => workSet.operationId === operationId);
276
383
  if (existing) {
277
384
  const projected = existing.entries.map(({ state: _state, updatedAtMs: _updated, issuedAtMs: _issued, permitOperationId: _permitOperation, permitIntentId: _permitIntent, permitLeaderEpoch: _permitEpoch, deletedAtMs: _deleted, ...candidate }) => candidate);
278
385
  if (!sameCandidates(projected, candidates))
@@ -282,21 +389,35 @@ export function createTelegramThreadCleanupWorkStore(options) {
282
389
  const atMs = now();
283
390
  if (!safeTime(atMs))
284
391
  throw new Error("Telegram Thread cleanup clock is invalid.");
285
- const workSet = validateWorkSet({ operationId, createdAtMs: atMs,
286
- entries: candidates.map(candidate => ({ ...structuredClone(candidate), state: "prepared", updatedAtMs: atMs })) }, options.profileName);
392
+ const workSet = validateWorkSet({
393
+ operationId,
394
+ createdAtMs: atMs,
395
+ entries: candidates.map((candidate) => ({
396
+ ...structuredClone(candidate),
397
+ state: "prepared",
398
+ updatedAtMs: atMs,
399
+ })),
400
+ }, options.profileName);
287
401
  publish({ ...file, workSets: [...file.workSets, workSet] });
288
402
  return { prepared: true, workSet: structuredClone(workSet) };
289
403
  });
290
404
  },
291
405
  recordDeletionIssued(input) {
292
- return mutate(file => {
293
- const workSet = file.workSets.find(candidate => candidate.operationId === input.operationId);
294
- const entry = workSet?.entries.find(candidate => candidate.bindingKey === input.bindingKey);
406
+ return mutate((file) => {
407
+ const workSet = file.workSets.find((candidate) => candidate.operationId === input.operationId);
408
+ const entry = workSet?.entries.find((candidate) => candidate.bindingKey === input.bindingKey);
295
409
  const permit = input.permit;
296
- if (!workSet || !entry || entry.bindingUpdatedAtMs !== input.bindingUpdatedAtMs ||
297
- permit.destructiveKind !== "manual-thread-cleanup" || permit.profileKey !== entry.profileName || permit.bindingKey !== entry.bindingKey ||
298
- permit.slot !== entry.slot || targetKey(permit.target) !== targetKey(entry.target) ||
299
- !permit.operationId || !permit.retirementIntentId || !safeTime(permit.issuedAtMs))
410
+ if (!workSet ||
411
+ !entry ||
412
+ entry.bindingUpdatedAtMs !== input.bindingUpdatedAtMs ||
413
+ permit.destructiveKind !== "manual-thread-cleanup" ||
414
+ permit.profileKey !== entry.profileName ||
415
+ permit.bindingKey !== entry.bindingKey ||
416
+ permit.slot !== entry.slot ||
417
+ targetKey(permit.target) !== targetKey(entry.target) ||
418
+ !permit.operationId ||
419
+ !permit.retirementIntentId ||
420
+ !safeTime(permit.issuedAtMs))
300
421
  throw new Error("Telegram Thread cleanup entry or deletion permit is stale or mismatched.");
301
422
  if (entry.state !== "prepared") {
302
423
  const exact = entry.permitOperationId === permit.operationId &&
@@ -307,7 +428,9 @@ export function createTelegramThreadCleanupWorkStore(options) {
307
428
  return { recorded: false, entry: structuredClone(entry) };
308
429
  }
309
430
  const atMs = now();
310
- if (!safeTime(atMs) || atMs < entry.updatedAtMs || atMs < permit.issuedAtMs)
431
+ if (!safeTime(atMs) ||
432
+ atMs < entry.updatedAtMs ||
433
+ atMs < permit.issuedAtMs)
311
434
  throw new Error("Telegram Thread cleanup clock is invalid.");
312
435
  entry.state = "outcome-unknown";
313
436
  entry.issuedAtMs = atMs;
@@ -320,9 +443,10 @@ export function createTelegramThreadCleanupWorkStore(options) {
320
443
  });
321
444
  },
322
445
  confirmDeleted(input) {
323
- return mutate(file => {
324
- const entry = file.workSets.find(candidate => candidate.operationId === input.operationId)
325
- ?.entries.find(candidate => candidate.bindingKey === input.bindingKey);
446
+ return mutate((file) => {
447
+ const entry = file.workSets
448
+ .find((candidate) => candidate.operationId === input.operationId)
449
+ ?.entries.find((candidate) => candidate.bindingKey === input.bindingKey);
326
450
  if (!entry)
327
451
  throw new Error("Telegram Thread cleanup entry is missing.");
328
452
  if (entry.state === "deleted")
@@ -345,15 +469,20 @@ export function createTelegramThreadCleanupWorkStore(options) {
345
469
  };
346
470
  }
347
471
  export async function commitTelegramInactiveThreadCleanup(input) {
348
- if (!await input.commitBinding())
472
+ if (!(await input.commitBinding()))
349
473
  return false;
350
- input.store.confirmDeleted({ operationId: input.operationId, bindingKey: input.bindingKey });
474
+ input.store.confirmDeleted({
475
+ operationId: input.operationId,
476
+ bindingKey: input.bindingKey,
477
+ });
351
478
  return true;
352
479
  }
353
480
  export async function executeTelegramInactiveThreadCleanup(input) {
354
481
  return input.withWorkspaceDeletionBoundary(async () => {
355
- const workSet = input.store.list().find(candidate => candidate.operationId === input.operationId);
356
- const retained = workSet?.entries.find(candidate => candidate.bindingKey === input.bindingKey);
482
+ const workSet = input.store
483
+ .list()
484
+ .find((candidate) => candidate.operationId === input.operationId);
485
+ const retained = workSet?.entries.find((candidate) => candidate.bindingKey === input.bindingKey);
357
486
  if (!retained)
358
487
  return { status: "blocked" };
359
488
  if (retained.state === "deleted")
@@ -361,33 +490,45 @@ export async function executeTelegramInactiveThreadCleanup(input) {
361
490
  if (retained.state === "outcome-unknown")
362
491
  return { status: "outcome-unknown", entry: retained };
363
492
  const planned = planTelegramInactiveThreadCleanup(await input.loadFreshEvidence());
364
- const fresh = planned.find(candidate => candidate.bindingKey === retained.bindingKey);
493
+ const fresh = planned.find((candidate) => candidate.bindingKey === retained.bindingKey);
365
494
  const retainedCandidate = (({ state: _state, updatedAtMs: _updated, issuedAtMs: _issued, permitOperationId: _permitOperation, permitIntentId: _permitIntent, permitLeaderEpoch: _permitEpoch, deletedAtMs: _deleted, ...candidate }) => candidate)(retained);
366
495
  if (!fresh || !sameCandidates([fresh], [retainedCandidate]))
367
496
  return { status: "blocked" };
368
497
  const permitResult = await input.acquireDeletionPermit(fresh);
369
498
  if (permitResult.kind !== "issued")
370
499
  return { status: "blocked" };
371
- const recorded = input.store.recordDeletionIssued({ operationId: input.operationId,
372
- bindingKey: input.bindingKey, bindingUpdatedAtMs: retained.bindingUpdatedAtMs,
373
- permit: permitResult.permit });
500
+ const recorded = input.store.recordDeletionIssued({
501
+ operationId: input.operationId,
502
+ bindingKey: input.bindingKey,
503
+ bindingUpdatedAtMs: retained.bindingUpdatedAtMs,
504
+ permit: permitResult.permit,
505
+ });
374
506
  if (!recorded.recorded)
375
507
  return { status: "outcome-unknown", entry: recorded.entry };
376
508
  await input.deleteWithPermit(permitResult.permit, fresh);
377
- const confirmed = input.store.confirmDeleted({ operationId: input.operationId,
378
- bindingKey: input.bindingKey });
509
+ const confirmed = input.store.confirmDeleted({
510
+ operationId: input.operationId,
511
+ bindingKey: input.bindingKey,
512
+ });
379
513
  return { status: "deleted", entry: confirmed.entry };
380
514
  });
381
515
  }
382
516
  export function createTelegramThreadCleanupPermitRuntime(deps) {
383
517
  const now = deps.getNowMs ?? Date.now;
384
518
  const findExactFence = (candidate, workSetId) => {
385
- const operationId = `thread-cleanup-fence:${createHash("sha256").update(`${workSetId}\u0000${candidate.bindingKey}`, "utf8").digest("hex")}`;
519
+ const operationId = `thread-cleanup-fence:${createHash("sha256")
520
+ .update(`${workSetId}\u0000${candidate.bindingKey}`, "utf8")
521
+ .digest("hex")}`;
386
522
  const fence = deps.ledger.read().fence;
387
- return fence?.operationId === operationId && fence.destructiveKind === "manual-thread-cleanup" &&
388
- fence.profileKey === candidate.profileName && fence.retirementIntentId === workSetId && fence.bindingKey === candidate.bindingKey &&
389
- fence.slot === candidate.slot && targetKey(fence.target) === targetKey(candidate.target)
390
- ? fence : undefined;
523
+ return fence?.operationId === operationId &&
524
+ fence.destructiveKind === "manual-thread-cleanup" &&
525
+ fence.profileKey === candidate.profileName &&
526
+ fence.retirementIntentId === workSetId &&
527
+ fence.bindingKey === candidate.bindingKey &&
528
+ fence.slot === candidate.slot &&
529
+ targetKey(fence.target) === targetKey(candidate.target)
530
+ ? fence
531
+ : undefined;
391
532
  };
392
533
  const adoptExactFence = (candidate, workSetId) => {
393
534
  let fence = findExactFence(candidate, workSetId);
@@ -395,12 +536,16 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
395
536
  if (!fence || leaderEpoch === undefined)
396
537
  return undefined;
397
538
  const owner = deps.getOwner();
398
- if (fence.leaderEpoch !== leaderEpoch || fence.owner.processId !== owner.processId ||
539
+ if (fence.leaderEpoch !== leaderEpoch ||
540
+ fence.owner.processId !== owner.processId ||
399
541
  fence.owner.processBirthId !== owner.processBirthId) {
400
542
  if (!deps.canAdoptFence(fence))
401
543
  return undefined;
402
544
  try {
403
- const adopted = deps.ledger.adoptThreadCleanupFence(fence, { owner, leaderEpoch });
545
+ const adopted = deps.ledger.adoptThreadCleanupFence(fence, {
546
+ owner,
547
+ leaderEpoch,
548
+ });
404
549
  if (adopted.destructiveKind !== "manual-thread-cleanup")
405
550
  return undefined;
406
551
  fence = adopted;
@@ -422,8 +567,11 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
422
567
  const owner = deps.getOwner();
423
568
  const leaderEpoch = deps.getLeaderEpoch();
424
569
  const owns = exact.owner.processId === owner.processId &&
425
- exact.owner.processBirthId === owner.processBirthId && exact.leaderEpoch === leaderEpoch;
426
- return owns || deps.canAdoptFence(exact) ? exact.phase : "authority-blocked";
570
+ exact.owner.processBirthId === owner.processBirthId &&
571
+ exact.leaderEpoch === leaderEpoch;
572
+ return owns || deps.canAdoptFence(exact)
573
+ ? exact.phase
574
+ : "authority-blocked";
427
575
  },
428
576
  findCommitReady(candidate, workSetId) {
429
577
  const fence = adoptExactFence(candidate, workSetId);
@@ -431,31 +579,46 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
431
579
  },
432
580
  async acquire(candidate, workSetId) {
433
581
  const leaderEpoch = deps.getLeaderEpoch();
434
- if (leaderEpoch === undefined || candidate.profileName.length === 0 ||
582
+ if (leaderEpoch === undefined ||
583
+ candidate.profileName.length === 0 ||
435
584
  candidate.profileName !== deps.getProfileName())
436
585
  return { kind: "blocked" };
437
586
  const operationId = `thread-cleanup-fence:${createHash("sha256")
438
- .update(workSetId).update("\0").update(candidate.bindingKey).digest("hex")}`;
587
+ .update(workSetId)
588
+ .update("\0")
589
+ .update(candidate.bindingKey)
590
+ .digest("hex")}`;
439
591
  const existing = adoptExactFence(candidate, workSetId) ?? deps.ledger.read().fence;
440
- if (existing && existing.operationId === operationId &&
592
+ if (existing &&
593
+ existing.operationId === operationId &&
441
594
  existing.destructiveKind === "manual-thread-cleanup" &&
442
- existing.retirementIntentId === workSetId && existing.bindingKey === candidate.bindingKey &&
443
- existing.slot === candidate.slot && targetKey(existing.target) === targetKey(candidate.target) &&
444
- existing.leaderEpoch === leaderEpoch && existing.phase !== "fenced") {
595
+ existing.retirementIntentId === workSetId &&
596
+ existing.bindingKey === candidate.bindingKey &&
597
+ existing.slot === candidate.slot &&
598
+ targetKey(existing.target) === targetKey(candidate.target) &&
599
+ existing.leaderEpoch === leaderEpoch &&
600
+ existing.phase !== "fenced") {
445
601
  return { kind: "already-issued" };
446
602
  }
447
603
  let acquired;
448
604
  try {
449
- acquired = deps.ledger.acquireThreadCleanupFence({ operationId,
450
- cleanupWorkSetId: workSetId, bindingKey: candidate.bindingKey, slot: candidate.slot,
451
- target: candidate.target, leaderEpoch, cleanupRequestedAtMs: now() });
605
+ acquired = deps.ledger.acquireThreadCleanupFence({
606
+ operationId,
607
+ cleanupWorkSetId: workSetId,
608
+ bindingKey: candidate.bindingKey,
609
+ slot: candidate.slot,
610
+ target: candidate.target,
611
+ leaderEpoch,
612
+ cleanupRequestedAtMs: now(),
613
+ });
452
614
  }
453
615
  catch {
454
616
  return { kind: "blocked" };
455
617
  }
456
618
  if (acquired.kind === "blocked")
457
619
  return { kind: "blocked" };
458
- if (deps.getLeaderEpoch() !== leaderEpoch || !await deps.revalidateUnderFence(candidate)) {
620
+ if (deps.getLeaderEpoch() !== leaderEpoch ||
621
+ !(await deps.revalidateUnderFence(candidate))) {
459
622
  if (acquired.fence.phase === "fenced")
460
623
  deps.ledger.releaseUnissuedThreadCleanupFence(acquired.fence);
461
624
  return { kind: "blocked" };
@@ -467,7 +630,7 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
467
630
  },
468
631
  async settleDeleted(fence, commit) {
469
632
  const ready = deps.ledger.confirmThreadCleanupAbsence(fence);
470
- if (!await commit())
633
+ if (!(await commit()))
471
634
  return "commit-pending";
472
635
  deps.ledger.completeThreadCleanupFence(ready);
473
636
  return "completed";
@@ -475,12 +638,14 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
475
638
  };
476
639
  }
477
640
  export function createTelegramInactiveThreadCleanupSettingsPort(deps) {
478
- return operationId => cleanReviewedInactiveThreads({ ...deps, operationId });
641
+ return (operationId) => cleanReviewedInactiveThreads({ ...deps, operationId });
479
642
  }
480
643
  export async function cleanReviewedInactiveThreads(input) {
481
644
  if (!/^thread-cleanup:[a-f0-9]{32}$/u.test(input.operationId))
482
645
  throw new Error("Telegram Thread cleanup confirmation identity is invalid.");
483
- const workSet = input.store.list().find(candidate => candidate.operationId === input.operationId);
646
+ const workSet = input.store
647
+ .list()
648
+ .find((candidate) => candidate.operationId === input.operationId);
484
649
  if (!workSet)
485
650
  throw new Error("Telegram Thread cleanup review is missing.");
486
651
  let deleted = 0;
@@ -500,8 +665,12 @@ export async function cleanReviewedInactiveThreads(input) {
500
665
  outcomeUnknown += 1;
501
666
  continue;
502
667
  }
503
- const settlement = await input.permitRuntime.settleDeleted(ready, () => commitTelegramInactiveThreadCleanup({ store: input.store, operationId: input.operationId,
504
- bindingKey: entry.bindingKey, commitBinding: () => input.commitBinding(entry) }));
668
+ const settlement = await input.permitRuntime.settleDeleted(ready, () => commitTelegramInactiveThreadCleanup({
669
+ store: input.store,
670
+ operationId: input.operationId,
671
+ bindingKey: entry.bindingKey,
672
+ commitBinding: () => input.commitBinding(entry),
673
+ }));
505
674
  if (settlement === "completed")
506
675
  deleted += 1;
507
676
  else
@@ -523,8 +692,10 @@ export async function cleanReviewedInactiveThreads(input) {
523
692
  continue;
524
693
  }
525
694
  try {
526
- const recorded = input.store.recordDeletionIssued({ operationId: input.operationId,
527
- bindingKey: entry.bindingKey, bindingUpdatedAtMs: entry.bindingUpdatedAtMs,
695
+ const recorded = input.store.recordDeletionIssued({
696
+ operationId: input.operationId,
697
+ bindingKey: entry.bindingKey,
698
+ bindingUpdatedAtMs: entry.bindingUpdatedAtMs,
528
699
  permit: authority.permit,
529
700
  });
530
701
  if (!recorded.recorded) {
@@ -540,8 +711,12 @@ export async function cleanReviewedInactiveThreads(input) {
540
711
  continue;
541
712
  }
542
713
  await input.deleteWithPermit(authority.permit, entry);
543
- const settlement = await input.permitRuntime.settleDeleted(authority.fence, () => commitTelegramInactiveThreadCleanup({ store: input.store, operationId: input.operationId,
544
- bindingKey: entry.bindingKey, commitBinding: () => input.commitBinding(entry, fullBinding) }));
714
+ const settlement = await input.permitRuntime.settleDeleted(authority.fence, () => commitTelegramInactiveThreadCleanup({
715
+ store: input.store,
716
+ operationId: input.operationId,
717
+ bindingKey: entry.bindingKey,
718
+ commitBinding: () => input.commitBinding(entry, fullBinding),
719
+ }));
545
720
  if (settlement === "completed")
546
721
  deleted += 1;
547
722
  else
@@ -551,11 +726,19 @@ export async function cleanReviewedInactiveThreads(input) {
551
726
  outcomeUnknown += 1;
552
727
  }
553
728
  }
554
- const retainedEntries = input.store.list().find(candidate => candidate.operationId === input.operationId)?.entries ?? [];
729
+ const retainedEntries = input.store
730
+ .list()
731
+ .find((candidate) => candidate.operationId === input.operationId)
732
+ ?.entries ?? [];
555
733
  if (outcomeUnknown > 0) {
556
- const diagnosis = retainedEntries.map(entry => input.permitRuntime.diagnoseRecovery(entry, input.operationId)).find(value => value !== "none") ?? "none";
557
- const recovery = diagnosis === "commit-ready" ? "commit-ready"
558
- : diagnosis === "deletion-issued" ? "deletion-outcome-unknown" : "authority-blocked";
734
+ const diagnosis = retainedEntries
735
+ .map((entry) => input.permitRuntime.diagnoseRecovery(entry, input.operationId))
736
+ .find((value) => value !== "none") ?? "none";
737
+ const recovery = diagnosis === "commit-ready"
738
+ ? "commit-ready"
739
+ : diagnosis === "deletion-issued"
740
+ ? "deletion-outcome-unknown"
741
+ : "authority-blocked";
559
742
  return { deleted, outcomeUnknown, blocked, recovery };
560
743
  }
561
744
  return { deleted, outcomeUnknown, blocked };