@jungjaehoon/mama-os 0.74.0 → 0.76.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 (254) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +6 -1
  3. package/dist/api/code-act-actions.d.ts +3 -1
  4. package/dist/api/code-act-actions.d.ts.map +1 -1
  5. package/dist/api/code-act-actions.js +16 -7
  6. package/dist/api/code-act-actions.js.map +1 -1
  7. package/dist/api/document-actions.d.ts +13 -0
  8. package/dist/api/document-actions.d.ts.map +1 -0
  9. package/dist/api/document-actions.js +55 -0
  10. package/dist/api/document-actions.js.map +1 -0
  11. package/dist/api/drive-actions.d.ts +3 -1
  12. package/dist/api/drive-actions.d.ts.map +1 -1
  13. package/dist/api/drive-actions.js +6 -1
  14. package/dist/api/drive-actions.js.map +1 -1
  15. package/dist/api/member-enrollment.d.ts +7 -4
  16. package/dist/api/member-enrollment.d.ts.map +1 -1
  17. package/dist/api/member-enrollment.js +65 -33
  18. package/dist/api/member-enrollment.js.map +1 -1
  19. package/dist/api/member-export.d.ts.map +1 -1
  20. package/dist/api/member-export.js +14 -6
  21. package/dist/api/member-export.js.map +1 -1
  22. package/dist/api/member-lifecycle.d.ts +23 -4
  23. package/dist/api/member-lifecycle.d.ts.map +1 -1
  24. package/dist/api/member-lifecycle.js +90 -19
  25. package/dist/api/member-lifecycle.js.map +1 -1
  26. package/dist/api/member-notice-actions.d.ts +18 -0
  27. package/dist/api/member-notice-actions.d.ts.map +1 -0
  28. package/dist/api/member-notice-actions.js +184 -0
  29. package/dist/api/member-notice-actions.js.map +1 -0
  30. package/dist/api/member-records.d.ts +17 -2
  31. package/dist/api/member-records.d.ts.map +1 -1
  32. package/dist/api/member-records.js +440 -82
  33. package/dist/api/member-records.js.map +1 -1
  34. package/dist/api/member-share-actions.d.ts +3 -3
  35. package/dist/api/member-share-actions.d.ts.map +1 -1
  36. package/dist/api/member-share-actions.js +49 -22
  37. package/dist/api/member-share-actions.js.map +1 -1
  38. package/dist/api/member-smoke-state.d.ts +3 -0
  39. package/dist/api/member-smoke-state.d.ts.map +1 -0
  40. package/dist/api/member-smoke-state.js +20 -0
  41. package/dist/api/member-smoke-state.js.map +1 -0
  42. package/dist/api/owner-policy-actions.d.ts +1 -0
  43. package/dist/api/owner-policy-actions.d.ts.map +1 -1
  44. package/dist/api/owner-policy-actions.js +8 -4
  45. package/dist/api/owner-policy-actions.js.map +1 -1
  46. package/dist/api/owner-timezone-actions.d.ts +3 -2
  47. package/dist/api/owner-timezone-actions.d.ts.map +1 -1
  48. package/dist/api/owner-timezone-actions.js +4 -6
  49. package/dist/api/owner-timezone-actions.js.map +1 -1
  50. package/dist/api/personal-job-actions.d.ts +6 -0
  51. package/dist/api/personal-job-actions.d.ts.map +1 -0
  52. package/dist/api/personal-job-actions.js +72 -0
  53. package/dist/api/personal-job-actions.js.map +1 -0
  54. package/dist/api/security-events.d.ts +6 -1
  55. package/dist/api/security-events.d.ts.map +1 -1
  56. package/dist/api/security-events.js +34 -15
  57. package/dist/api/security-events.js.map +1 -1
  58. package/dist/api/skill-actions.d.ts +5 -0
  59. package/dist/api/skill-actions.d.ts.map +1 -0
  60. package/dist/api/skill-actions.js +66 -0
  61. package/dist/api/skill-actions.js.map +1 -0
  62. package/dist/api/source-actions.d.ts.map +1 -1
  63. package/dist/api/source-actions.js +4 -2
  64. package/dist/api/source-actions.js.map +1 -1
  65. package/dist/api/stored-source-reader.d.ts.map +1 -1
  66. package/dist/api/stored-source-reader.js +9 -2
  67. package/dist/api/stored-source-reader.js.map +1 -1
  68. package/dist/api/trello-actions.d.ts +1 -1
  69. package/dist/api/trello-actions.d.ts.map +1 -1
  70. package/dist/api/trello-actions.js +19 -4
  71. package/dist/api/trello-actions.js.map +1 -1
  72. package/dist/api/work-actions.d.ts +12 -2
  73. package/dist/api/work-actions.d.ts.map +1 -1
  74. package/dist/api/work-actions.js +57 -10
  75. package/dist/api/work-actions.js.map +1 -1
  76. package/dist/cli/commands/daemon.d.ts +12 -2
  77. package/dist/cli/commands/daemon.d.ts.map +1 -1
  78. package/dist/cli/commands/daemon.js +80 -13
  79. package/dist/cli/commands/daemon.js.map +1 -1
  80. package/dist/cli/commands/init.d.ts +4 -0
  81. package/dist/cli/commands/init.d.ts.map +1 -1
  82. package/dist/cli/commands/init.js +27 -5
  83. package/dist/cli/commands/init.js.map +1 -1
  84. package/dist/cli/commands/register-owner.d.ts +14 -5
  85. package/dist/cli/commands/register-owner.d.ts.map +1 -1
  86. package/dist/cli/commands/register-owner.js +145 -42
  87. package/dist/cli/commands/register-owner.js.map +1 -1
  88. package/dist/cli/commands/tools.d.ts +12 -0
  89. package/dist/cli/commands/tools.d.ts.map +1 -0
  90. package/dist/cli/commands/tools.js +109 -0
  91. package/dist/cli/commands/tools.js.map +1 -0
  92. package/dist/cli/index.js +8 -4
  93. package/dist/cli/index.js.map +1 -1
  94. package/dist/connectors/framework/polling-scheduler.d.ts +6 -1
  95. package/dist/connectors/framework/polling-scheduler.d.ts.map +1 -1
  96. package/dist/connectors/framework/polling-scheduler.js +25 -3
  97. package/dist/connectors/framework/polling-scheduler.js.map +1 -1
  98. package/dist/connectors/framework/stored-index-read.d.ts +2 -2
  99. package/dist/connectors/framework/stored-index-read.d.ts.map +1 -1
  100. package/dist/connectors/framework/stored-index-read.js +10 -3
  101. package/dist/connectors/framework/stored-index-read.js.map +1 -1
  102. package/dist/gateways/base-gateway.d.ts +7 -0
  103. package/dist/gateways/base-gateway.d.ts.map +1 -1
  104. package/dist/gateways/base-gateway.js +24 -0
  105. package/dist/gateways/base-gateway.js.map +1 -1
  106. package/dist/gateways/discord.d.ts +4 -0
  107. package/dist/gateways/discord.d.ts.map +1 -1
  108. package/dist/gateways/discord.js +24 -6
  109. package/dist/gateways/discord.js.map +1 -1
  110. package/dist/gateways/slack.d.ts +3 -0
  111. package/dist/gateways/slack.d.ts.map +1 -1
  112. package/dist/gateways/slack.js +15 -2
  113. package/dist/gateways/slack.js.map +1 -1
  114. package/dist/gateways/telegram-errors.d.ts +3 -0
  115. package/dist/gateways/telegram-errors.d.ts.map +1 -1
  116. package/dist/gateways/telegram-errors.js +5 -0
  117. package/dist/gateways/telegram-errors.js.map +1 -1
  118. package/dist/gateways/telegram-message-ledger.d.ts +7 -0
  119. package/dist/gateways/telegram-message-ledger.d.ts.map +1 -1
  120. package/dist/gateways/telegram-message-ledger.js +27 -2
  121. package/dist/gateways/telegram-message-ledger.js.map +1 -1
  122. package/dist/gateways/telegram.d.ts +33 -7
  123. package/dist/gateways/telegram.d.ts.map +1 -1
  124. package/dist/gateways/telegram.js +297 -59
  125. package/dist/gateways/telegram.js.map +1 -1
  126. package/dist/gateways/turn-contract.d.ts +8 -0
  127. package/dist/gateways/turn-contract.d.ts.map +1 -1
  128. package/dist/runtime/action-surface.d.ts +28 -3
  129. package/dist/runtime/action-surface.d.ts.map +1 -1
  130. package/dist/runtime/action-surface.js +316 -39
  131. package/dist/runtime/action-surface.js.map +1 -1
  132. package/dist/runtime/backend-security.d.ts +4 -0
  133. package/dist/runtime/backend-security.d.ts.map +1 -1
  134. package/dist/runtime/backend-security.js +8 -1
  135. package/dist/runtime/backend-security.js.map +1 -1
  136. package/dist/runtime/config.d.ts +1 -1
  137. package/dist/runtime/config.d.ts.map +1 -1
  138. package/dist/runtime/config.js +18 -11
  139. package/dist/runtime/config.js.map +1 -1
  140. package/dist/runtime/connectors.js +1 -1
  141. package/dist/runtime/connectors.js.map +1 -1
  142. package/dist/runtime/egress-proxy.d.ts +3 -3
  143. package/dist/runtime/egress-proxy.d.ts.map +1 -1
  144. package/dist/runtime/egress-proxy.js +16 -3
  145. package/dist/runtime/egress-proxy.js.map +1 -1
  146. package/dist/runtime/member-paths.d.ts +9 -1
  147. package/dist/runtime/member-paths.d.ts.map +1 -1
  148. package/dist/runtime/member-paths.js +67 -5
  149. package/dist/runtime/member-paths.js.map +1 -1
  150. package/dist/runtime/member-session.d.ts +21 -21
  151. package/dist/runtime/member-session.d.ts.map +1 -1
  152. package/dist/runtime/member-session.js +289 -34
  153. package/dist/runtime/member-session.js.map +1 -1
  154. package/dist/runtime/member-system-prompt.d.ts +1 -0
  155. package/dist/runtime/member-system-prompt.d.ts.map +1 -1
  156. package/dist/runtime/member-system-prompt.js +26 -4
  157. package/dist/runtime/member-system-prompt.js.map +1 -1
  158. package/dist/runtime/native-session.d.ts +6 -0
  159. package/dist/runtime/native-session.d.ts.map +1 -1
  160. package/dist/runtime/native-session.js +80 -13
  161. package/dist/runtime/native-session.js.map +1 -1
  162. package/dist/runtime/outbound-attempts.d.ts +5 -2
  163. package/dist/runtime/outbound-attempts.d.ts.map +1 -1
  164. package/dist/runtime/outbound-attempts.js +3 -2
  165. package/dist/runtime/outbound-attempts.js.map +1 -1
  166. package/dist/runtime/owner-authority.d.ts +15 -13
  167. package/dist/runtime/owner-authority.d.ts.map +1 -1
  168. package/dist/runtime/owner-authority.js +164 -8
  169. package/dist/runtime/owner-authority.js.map +1 -1
  170. package/dist/runtime/owner-runtime.d.ts +19 -4
  171. package/dist/runtime/owner-runtime.d.ts.map +1 -1
  172. package/dist/runtime/owner-runtime.js +446 -44
  173. package/dist/runtime/owner-runtime.js.map +1 -1
  174. package/dist/runtime/owner-system-prompt.d.ts +3 -1
  175. package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
  176. package/dist/runtime/owner-system-prompt.js +15 -5
  177. package/dist/runtime/owner-system-prompt.js.map +1 -1
  178. package/dist/runtime/personal-job-cron.d.ts +6 -0
  179. package/dist/runtime/personal-job-cron.d.ts.map +1 -0
  180. package/dist/runtime/personal-job-cron.js +154 -0
  181. package/dist/runtime/personal-job-cron.js.map +1 -0
  182. package/dist/runtime/personal-jobs.d.ts +107 -0
  183. package/dist/runtime/personal-jobs.d.ts.map +1 -0
  184. package/dist/runtime/personal-jobs.js +353 -0
  185. package/dist/runtime/personal-jobs.js.map +1 -0
  186. package/dist/runtime/personal-notices.d.ts +38 -0
  187. package/dist/runtime/personal-notices.d.ts.map +1 -0
  188. package/dist/runtime/personal-notices.js +137 -0
  189. package/dist/runtime/personal-notices.js.map +1 -0
  190. package/dist/runtime/personal-turns.d.ts +23 -0
  191. package/dist/runtime/personal-turns.d.ts.map +1 -0
  192. package/dist/runtime/personal-turns.js +51 -0
  193. package/dist/runtime/personal-turns.js.map +1 -0
  194. package/dist/runtime/principal-access.d.ts +8 -0
  195. package/dist/runtime/principal-access.d.ts.map +1 -1
  196. package/dist/runtime/principal-access.js +99 -4
  197. package/dist/runtime/principal-access.js.map +1 -1
  198. package/dist/runtime/principal-sessions.d.ts +16 -2
  199. package/dist/runtime/principal-sessions.d.ts.map +1 -1
  200. package/dist/runtime/principal-sessions.js +104 -21
  201. package/dist/runtime/principal-sessions.js.map +1 -1
  202. package/dist/runtime/skills.d.ts +86 -0
  203. package/dist/runtime/skills.d.ts.map +1 -0
  204. package/dist/runtime/skills.js +560 -0
  205. package/dist/runtime/skills.js.map +1 -0
  206. package/dist/runtime/stimulus-delivery.d.ts +22 -1
  207. package/dist/runtime/stimulus-delivery.d.ts.map +1 -1
  208. package/dist/runtime/stimulus-delivery.js +192 -19
  209. package/dist/runtime/stimulus-delivery.js.map +1 -1
  210. package/dist/runtime/tools-paths.d.ts +9 -0
  211. package/dist/runtime/tools-paths.d.ts.map +1 -0
  212. package/dist/runtime/tools-paths.js +98 -0
  213. package/dist/runtime/tools-paths.js.map +1 -0
  214. package/dist/runtime/turn-orders.d.ts +22 -2
  215. package/dist/runtime/turn-orders.d.ts.map +1 -1
  216. package/dist/runtime/turn-orders.js +60 -6
  217. package/dist/runtime/turn-orders.js.map +1 -1
  218. package/dist/storage/chat-sources.d.ts +21 -0
  219. package/dist/storage/chat-sources.d.ts.map +1 -1
  220. package/dist/storage/chat-sources.js +66 -4
  221. package/dist/storage/chat-sources.js.map +1 -1
  222. package/dist/storage/source-archive.d.ts +13 -0
  223. package/dist/storage/source-archive.d.ts.map +1 -1
  224. package/dist/storage/source-archive.js +103 -0
  225. package/dist/storage/source-archive.js.map +1 -1
  226. package/dist/tools/command-runner.d.ts +12 -0
  227. package/dist/tools/command-runner.d.ts.map +1 -0
  228. package/dist/tools/command-runner.js +65 -0
  229. package/dist/tools/command-runner.js.map +1 -0
  230. package/dist/tools/document-render.d.ts +24 -0
  231. package/dist/tools/document-render.d.ts.map +1 -0
  232. package/dist/tools/document-render.js +208 -0
  233. package/dist/tools/document-render.js.map +1 -0
  234. package/dist/tools/document-toolkit.d.ts +15 -0
  235. package/dist/tools/document-toolkit.d.ts.map +1 -0
  236. package/dist/tools/document-toolkit.js +501 -0
  237. package/dist/tools/document-toolkit.js.map +1 -0
  238. package/dist/tools/office-render.d.ts +28 -0
  239. package/dist/tools/office-render.d.ts.map +1 -0
  240. package/dist/tools/office-render.js +98 -0
  241. package/dist/tools/office-render.js.map +1 -0
  242. package/dist/tools/toolkit-process.d.ts +9 -0
  243. package/dist/tools/toolkit-process.d.ts.map +1 -0
  244. package/dist/tools/toolkit-process.js +148 -0
  245. package/dist/tools/toolkit-process.js.map +1 -0
  246. package/dist/tools/toolkit-state.d.ts +22 -0
  247. package/dist/tools/toolkit-state.d.ts.map +1 -0
  248. package/dist/tools/toolkit-state.js +116 -0
  249. package/dist/tools/toolkit-state.js.map +1 -0
  250. package/package.json +3 -2
  251. package/toolkit/inspect-office-zip.py +16 -0
  252. package/toolkit/rasterize.py +29 -0
  253. package/toolkit/requirements.lock +664 -0
  254. package/toolkit/smoke.py +32 -0
@@ -1,8 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TelegramGateway = void 0;
4
+ const personal_turns_js_1 = require("../runtime/personal-turns.js");
4
5
  const node_async_hooks_1 = require("node:async_hooks");
5
6
  const node_crypto_1 = require("node:crypto");
7
+ const owner_authority_js_1 = require("../runtime/owner-authority.js");
8
+ const principal_access_js_1 = require("../runtime/principal-access.js");
6
9
  const node_fs_1 = require("node:fs");
7
10
  const node_path_1 = require("node:path");
8
11
  const grammy_1 = require("grammy");
@@ -65,13 +68,13 @@ function chatIdFromSourceMessageRef(value) {
65
68
  }
66
69
  return rest.slice(0, separator);
67
70
  }
68
- function outboundLedgerKey(idempotencyKey) {
69
- return `outbound:${(0, node_crypto_1.createHash)('sha256').update(`text\0${idempotencyKey}`).digest('hex')}`;
70
- }
71
- /** Telegram owner ingress and its durable response transport. */
71
+ /** Telegram personal/common ingress and its durable response transport. */
72
72
  class TelegramGateway extends base_gateway_js_1.BaseGateway {
73
73
  source = 'telegram';
74
74
  token;
75
+ registry;
76
+ memberIntake;
77
+ principalPaths;
75
78
  config;
76
79
  filesRoot;
77
80
  downloadsDir;
@@ -80,11 +83,17 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
80
83
  onFatalError;
81
84
  messageLedger;
82
85
  chatTails = new Map();
86
+ pendingMemberIntakes = new Map();
87
+ stopIntake;
88
+ intakeStopped = new Promise((resolve) => {
89
+ this.stopIntake = () => resolve(undefined);
90
+ });
91
+ enrollmentCompletions = new Set();
83
92
  activePresenters = new Map();
84
93
  recentMessageIds = new Map();
85
94
  activeChat = new node_async_hooks_1.AsyncLocalStorage();
86
95
  nextEnrollmentRequestId = (0, node_crypto_1.randomInt)(1, 0x7fffffff);
87
- pendingEnrollment;
96
+ pendingEnrollments = new Map();
88
97
  onMemberSelection;
89
98
  recordMemberSelection;
90
99
  bot = null;
@@ -96,14 +105,14 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
96
105
  constructor(options) {
97
106
  super({ intake: options.intake });
98
107
  this.token = options.token;
108
+ this.registry = options.registry;
109
+ this.memberIntake = options.memberIntake;
110
+ this.principalPaths = options.principalPaths;
99
111
  this.onMemberSelection = options.onMemberSelection;
100
112
  this.recordMemberSelection = options.recordMemberSelection;
101
113
  this.config = {
102
114
  enabled: options.config?.enabled ?? true,
103
115
  allowedChats: options.config?.allowedChats ?? [],
104
- ...(options.config?.ownerUserIds === undefined
105
- ? {}
106
- : { ownerUserIds: options.config.ownerUserIds }),
107
116
  ...(options.config?.ownerChatId === undefined
108
117
  ? {}
109
118
  : { ownerChatId: options.config.ownerChatId }),
@@ -130,6 +139,10 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
130
139
  async start() {
131
140
  if (this.connected)
132
141
  return;
142
+ this.accepting = true;
143
+ this.intakeStopped = new Promise((resolve) => {
144
+ this.stopIntake = () => resolve(undefined);
145
+ });
133
146
  if (!this.config.allowedChats?.some((chatId) => chatId.trim().length > 0)) {
134
147
  throw new Error('telegram gateway disabled: allowed_chats is not set. Run: mama status');
135
148
  }
@@ -174,10 +187,21 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
174
187
  throw error;
175
188
  }
176
189
  }
177
- async stop() {
178
- this.pendingEnrollment = undefined;
190
+ async beginStop() {
191
+ this.accepting = false;
192
+ this.stopIntake();
179
193
  if (this.bot)
180
194
  await this.bot.stop().catch(() => { });
195
+ }
196
+ async drain() {
197
+ await super.drain();
198
+ await Promise.allSettled([...this.enrollmentCompletions]);
199
+ await Promise.allSettled([...this.chatTails.values()]);
200
+ }
201
+ async stop() {
202
+ await this.beginStop();
203
+ await this.drain();
204
+ this.pendingEnrollments.clear();
181
205
  this.bot = null;
182
206
  this.connected = false;
183
207
  this.recentMessageIds.clear();
@@ -187,10 +211,9 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
187
211
  answered(sourceRef) {
188
212
  return this.messageLedger.get(sourceRef)?.state === 'delivered';
189
213
  }
190
- /** Final response callback used by the owner runtime after a native turn settles. */
214
+ /** Final response callback used after a common or personal native turn settles. */
191
215
  async deliverResponse(sourceRef, response) {
192
216
  const chatId = chatIdFromSourceMessageRef(sourceRef);
193
- this.requireAllowedChat(chatId);
194
217
  const existing = this.messageLedger.get(sourceRef);
195
218
  if (!existing)
196
219
  throw new Error(`Telegram response has no accepted message ${sourceRef}`);
@@ -199,6 +222,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
199
222
  }
200
223
  if (existing.state === 'delivered')
201
224
  return;
225
+ await this.responseIntake(sourceRef);
202
226
  if (existing.state === 'ready' && existing.response !== response) {
203
227
  throw new Error('Telegram response conflicts with its durable ledger entry');
204
228
  }
@@ -208,6 +232,18 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
208
232
  return;
209
233
  await this.deliverReadyEntry(sourceRef);
210
234
  }
235
+ /** Close one accepted input with the configured host notice, preserving any ready reply. */
236
+ async interruptResponse(sourceRef) {
237
+ const entry = this.messageLedger.get(sourceRef);
238
+ if (!entry)
239
+ throw new Error(`Telegram interruption has no accepted message ${sourceRef}`);
240
+ if (entry.state === 'delivered')
241
+ return;
242
+ await this.responseIntake(sourceRef);
243
+ if (entry.state === 'processing')
244
+ this.messageLedger.markInterrupted(sourceRef, this.interruptedNotice, 'html-v1');
245
+ await this.deliverReadyEntry(sourceRef);
246
+ }
211
247
  async sendMessage(chatId, text, idempotencyKey) {
212
248
  if (!this.bot || !this.connected)
213
249
  throw new Error('Telegram gateway not connected');
@@ -224,6 +260,19 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
224
260
  await this.sendMessage(ownerChatId, text, idempotencyKey);
225
261
  }
226
262
  /** Host receipt only. The dispatcher never exposes arbitrary member text/destination input. */
263
+ memberTextDelivered(access, operationId) {
264
+ const destinations = access.destinations?.filter((target) => target.kind === 'telegram');
265
+ if (destinations?.length !== 1 || !destinations[0].id.trim())
266
+ throw new Error('Member text delivery requires exactly one Telegram destination');
267
+ if (!operationId.trim())
268
+ throw new Error('Member text operation id is required');
269
+ const entry = this.messageLedger.get((0, telegram_message_ledger_js_1.outboundLedgerKey)(operationId));
270
+ return (entry?.state === 'delivered' && entry.deliveryTarget === `telegram:${destinations[0].id}`);
271
+ }
272
+ /** Host-only retirement after the mailbox occurrence closes, including inactive recipients. */
273
+ retireMemberJobText(operationId) {
274
+ (0, telegram_message_ledger_js_1.retirePersonalTurnReceipt)(this.messageLedger, operationId);
275
+ }
227
276
  async sendMemberText(access, text, operationId) {
228
277
  const destinations = access.destinations?.filter((target) => target.kind === 'telegram');
229
278
  if (destinations?.length !== 1 || !destinations[0].id.trim())
@@ -234,9 +283,12 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
234
283
  throw new Error('Member text operation id is required');
235
284
  const chatId = destinations[0].id;
236
285
  await this.runInChatQueue(chatId, () => this.sendMessageNow(chatId, text, operationId));
237
- return this.messageLedger.get(outboundLedgerKey(operationId))?.state === 'delivered';
286
+ return this.memberTextDelivered(access, operationId);
287
+ }
288
+ sendFile(path, caption, operationId, member) {
289
+ return this.trackDelivery(() => this.sendFileNow(path, caption, operationId, member));
238
290
  }
239
- async sendFile(path, caption, operationId, member) {
291
+ async sendFileNow(path, caption, operationId, member) {
240
292
  if (!this.bot || !this.connected)
241
293
  throw new Error('Telegram gateway not connected');
242
294
  if (operationId.trim() === '')
@@ -249,6 +301,8 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
249
301
  if (destinations?.length !== 1 || !destinations[0].id.trim())
250
302
  throw new Error('Member file delivery requires exactly one Telegram destination');
251
303
  chatId = destinations[0].id;
304
+ if ((0, principal_access_js_1.roomChannel)(member.access) !== undefined)
305
+ this.requireAllowedChat(chatId);
252
306
  filesRoot = member.filesRoot;
253
307
  }
254
308
  else {
@@ -300,21 +354,26 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
300
354
  }
301
355
  }
302
356
  /** The source ref was verified as an owner turn by the runtime; only an owner DM can select. */
303
- async requestMemberEnrollment(sourceRef) {
357
+ async requestMemberEnrollment(sourceRef, requestingOwnerUserId, name) {
304
358
  if (!sourceRef.startsWith('telegram:'))
305
359
  throw Object.assign(new Error("Enrollment requires the owner's own Telegram DM"), {
306
360
  name: 'denied',
307
361
  });
308
- const ownerUserId = chatIdFromSourceMessageRef(sourceRef);
362
+ const ownerUserId = requestingOwnerUserId;
309
363
  const chatId = Number(ownerUserId);
310
- if (!this.bot || !this.onMemberSelection || !this.ownerAllowed(ownerUserId, ownerUserId)) {
364
+ if (!this.bot || !this.onMemberSelection || !this.ownerAllowed(ownerUserId)) {
311
365
  throw Object.assign(new Error("Enrollment requires the owner's own Telegram DM"), {
312
366
  name: 'denied',
313
367
  });
314
368
  }
315
369
  const requestId = this.nextEnrollmentRequestId;
316
370
  this.nextEnrollmentRequestId = requestId === 0x7fffffff ? 1 : requestId + 1;
317
- this.pendingEnrollment = { requestId, sourceMessageRef: sourceRef, ownerUserId };
371
+ this.pendingEnrollments.set(ownerUserId, {
372
+ requestId,
373
+ sourceMessageRef: sourceRef,
374
+ ownerUserId,
375
+ ...(name === undefined ? {} : { name }),
376
+ });
318
377
  // Telegram shows a request_users button only on the reply keyboard, under the message box.
319
378
  const prompt = 'Choose one member to enroll with the Choose member button under the message box, not in the chat. If it is hidden, open it with the keyboard icon in the message box.';
320
379
  try {
@@ -334,15 +393,24 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
334
393
  });
335
394
  }
336
395
  catch (error) {
337
- if (this.pendingEnrollment?.requestId === requestId)
338
- this.pendingEnrollment = undefined;
396
+ if (this.pendingEnrollments.get(ownerUserId)?.requestId === requestId)
397
+ this.pendingEnrollments.delete(ownerUserId);
339
398
  throw error;
340
399
  }
341
400
  }
342
- async completeMemberSelection(message) {
401
+ cancelMemberEnrollment(principalId) {
402
+ for (const ownerUserId of this.pendingEnrollments.keys()) {
403
+ if (this.registry.resolveByExternal('telegram', 'private', ownerUserId)?.principalId ===
404
+ principalId)
405
+ this.pendingEnrollments.delete(ownerUserId);
406
+ }
407
+ }
408
+ async completeMemberSelection(message, senderPrincipalId) {
343
409
  const chatId = String(message.chat.id);
344
410
  const ownerUserId = String(message.from.id);
345
- if (message.chat.type !== 'private' || chatId !== ownerUserId)
411
+ if (message.chat.type !== 'private' ||
412
+ chatId !== ownerUserId ||
413
+ !this.ownerAllowed(ownerUserId))
346
414
  return;
347
415
  let receiptText = 'Enrollment selection received.';
348
416
  let deliveryVerified = false;
@@ -355,7 +423,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
355
423
  deliveryVerified = true;
356
424
  };
357
425
  try {
358
- const pending = this.pendingEnrollment;
426
+ const pending = this.pendingEnrollments.get(ownerUserId);
359
427
  const shared = message.users_shared;
360
428
  if (!pending ||
361
429
  shared.request_id !== pending.requestId ||
@@ -364,7 +432,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
364
432
  return;
365
433
  }
366
434
  // Consume before awaiting the serial host chain, so concurrent duplicate updates cannot enroll.
367
- this.pendingEnrollment = undefined;
435
+ this.pendingEnrollments.delete(ownerUserId);
368
436
  if (shared.users.length !== 1 ||
369
437
  !Number.isSafeInteger(shared.users[0].user_id) ||
370
438
  shared.users[0].user_id <= 0) {
@@ -378,6 +446,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
378
446
  sourceMessageRef: pending.sourceMessageRef,
379
447
  ownerUserId,
380
448
  userId: String(shared.users[0].user_id),
449
+ ...(pending.name === undefined ? {} : { name: pending.name }),
381
450
  });
382
451
  }
383
452
  catch (error) {
@@ -411,6 +480,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
411
480
  message: {
412
481
  id: sourceMessageRef(chatId, message.message_id),
413
482
  channelKey: chatId,
483
+ senderPrincipalId,
414
484
  occurredAt: message.date * 1000,
415
485
  text: 'Member selection for enrollment (Choose member button)',
416
486
  // manage.member.list finds the receipts by this marker, not by the text.
@@ -431,31 +501,125 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
431
501
  getLastMessageAt() {
432
502
  return this.lastMessageAt;
433
503
  }
434
- ownerAllowed(chatId, userId) {
435
- if (!this.config.allowedChats?.includes(chatId))
436
- return false;
437
- if (this.config.ownerUserIds !== undefined)
438
- return this.config.ownerUserIds.includes(userId);
439
- return this.config.allowedChats.length === 1 && this.config.allowedChats[0] === userId;
504
+ ownerAllowed(userId) {
505
+ const principal = this.registry.resolveByExternal('telegram', 'private', userId);
506
+ return (principal?.kind === 'member' &&
507
+ principal.status === 'active' &&
508
+ (0, owner_authority_js_1.holdsOwnerRole)(this.registry, principal.principalId));
509
+ }
510
+ async responseIntake(sourceRef) {
511
+ const chatId = chatIdFromSourceMessageRef(sourceRef);
512
+ const recipient = this.registry.resolveByExternal('telegram', 'private', chatId);
513
+ if (recipient) {
514
+ if (recipient.kind !== 'member' || recipient.status !== 'active') {
515
+ throw new Error('Telegram DM response requires its registered active member');
516
+ }
517
+ return this.memberIntake(recipient.principalId);
518
+ }
519
+ this.requireAllowedChat(chatId);
520
+ // A room turn records its member on the ledger; a common message handed to a member's room
521
+ // is found through the durable room input, so a crash between the two cannot misroute it.
522
+ const principalId = this.messageLedger.get(sourceRef)?.principalId ?? this.intake.handedOffTo?.(sourceRef);
523
+ if (principalId !== undefined) {
524
+ const member = this.registry.findById(principalId);
525
+ if (member?.kind !== 'member' || member.status !== 'active')
526
+ throw new Error('Telegram room response requires its recorded active member');
527
+ return this.memberIntake(principalId, chatId);
528
+ }
529
+ return this.intake;
440
530
  }
441
531
  requireAllowedChat(chatId) {
442
532
  if (!this.config.allowedChats?.includes(chatId)) {
443
533
  throw new Error('Telegram destination is not allowlisted');
444
534
  }
445
535
  }
446
- async handleMessage(message) {
536
+ handleMessage(message) {
537
+ return this.trackIntake(() => this.handleMessageInput(message));
538
+ }
539
+ async handleMessageInput(message) {
447
540
  if (!message.chat || !message.from)
448
541
  return;
449
542
  const chatId = String(message.chat.id);
450
543
  const userId = String(message.from.id);
451
- if (message.from.is_bot || !this.ownerAllowed(chatId, userId)) {
544
+ const principal = this.registry.resolveByExternal('telegram', 'private', userId);
545
+ const privateChat = message.chat.type === 'private';
546
+ const admitted = principal?.kind === 'member' &&
547
+ principal.status === 'active' &&
548
+ (privateChat || this.config.allowedChats?.includes(chatId));
549
+ if (message.from.is_bot || !admitted) {
550
+ if (principal?.status === 'suspended')
551
+ this.cancelMemberEnrollment(principal.principalId);
452
552
  const chatHash = (0, node_crypto_1.createHash)('sha256').update(`chat\0${chatId}`).digest('hex');
453
553
  const senderHash = (0, node_crypto_1.createHash)('sha256').update(`sender\0${userId}`).digest('hex');
454
554
  console.warn(`telegram message dropped reason=non_owner chat_hash=${chatHash} sender_hash=${senderHash}`);
455
555
  return;
456
556
  }
457
557
  if (message.users_shared) {
458
- await this.completeMemberSelection(message);
558
+ // Enrollment can wait for running member turns. Let polling admit other updates while
559
+ // the host completion sends and records its own final receipt.
560
+ const completion = this.completeMemberSelection(message, principal.principalId).catch((error) => this.log(`telegram enrollment completion failed error=${telegramErrorMessage(error)}`));
561
+ this.enrollmentCompletions.add(completion);
562
+ void completion.finally(() => this.enrollmentCompletions.delete(completion));
563
+ return;
564
+ }
565
+ const principalId = principal.principalId;
566
+ const recorded = this.messageLedger.get(sourceMessageRef(chatId, message.message_id));
567
+ const memberRoom = !privateChat && (recorded ? recorded.principalId !== undefined : !this.ownerAllowed(userId));
568
+ const selectedIntake = () => privateChat || memberRoom
569
+ ? this.memberIntake(principalId, memberRoom ? chatId : undefined)
570
+ : this.intake;
571
+ const queued = (intake) => {
572
+ const selected = (0, telegram_text_entities_js_1.selectTelegramTextEntities)(message);
573
+ const files = (0, telegram_attachments_js_1.telegramFiles)(message);
574
+ if (!selected.text.trim() && files.length === 0)
575
+ return;
576
+ const ref = sourceMessageRef(chatId, message.message_id);
577
+ const claim = this.claimMessage(message, privateChat || memberRoom ? principalId : undefined);
578
+ this.pendingMemberIntakes.set(chatId, (this.pendingMemberIntakes.get(chatId) ?? 0) + 1);
579
+ void this.runInChatQueue(chatId, async () => {
580
+ const target = await Promise.race([
581
+ Promise.resolve(intake ?? selectedIntake()),
582
+ this.intakeStopped,
583
+ ]);
584
+ // The claimed inbound ledger already preserves this wait. Stop leaves it for startup
585
+ // interruption recovery rather than waiting for a directory/model slot to open.
586
+ if (!target || !this.accepting)
587
+ return;
588
+ await this.acceptMessage(message, principalId, target, claim.claimed ? ref : undefined);
589
+ })
590
+ .catch((error) => {
591
+ this.lastError = telegramErrorMessage(error);
592
+ this.log(`telegram deferred intake failed error=${this.lastError}`);
593
+ })
594
+ .finally(() => {
595
+ const pending = this.pendingMemberIntakes.get(chatId) - 1;
596
+ if (pending)
597
+ this.pendingMemberIntakes.set(chatId, pending);
598
+ else
599
+ this.pendingMemberIntakes.delete(chatId);
600
+ });
601
+ };
602
+ // While an intake for this chat waits, every later message queues behind it, whichever
603
+ // session it selects, so the group's order holds.
604
+ if (this.pendingMemberIntakes.has(chatId)) {
605
+ queued();
606
+ return;
607
+ }
608
+ const intake = selectedIntake();
609
+ if ('then' in intake) {
610
+ queued(intake);
611
+ return;
612
+ }
613
+ await this.acceptMessage(message, principalId, intake);
614
+ }
615
+ async acceptMessage(message, principalId, intake, queuedClaim) {
616
+ const chatId = String(message.chat.id);
617
+ const privateChat = message.chat.type === 'private';
618
+ const principal = this.registry.resolveByExternal('telegram', 'private', String(message.from.id));
619
+ if (principal?.principalId !== principalId || principal.status !== 'active') {
620
+ const chatHash = (0, node_crypto_1.createHash)('sha256').update(`chat\0${chatId}`).digest('hex');
621
+ const senderHash = (0, node_crypto_1.createHash)('sha256').update(`sender\0${message.from.id}`).digest('hex');
622
+ console.warn(`telegram message dropped reason=principal_inactive chat_hash=${chatHash} sender_hash=${senderHash}`);
459
623
  return;
460
624
  }
461
625
  const ref = sourceMessageRef(chatId, message.message_id);
@@ -477,11 +641,10 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
477
641
  }
478
642
  if (durable?.state === 'processing' && this.activePresenters.has(ref))
479
643
  return;
480
- if (durable?.state === 'processing' && this.intake.isPending?.(ref))
644
+ if (durable?.state === 'processing' && intake.isPending?.(ref))
481
645
  return;
482
- if (durable?.state === 'processing') {
483
- this.messageLedger.markInterrupted(ref, this.interruptedNotice, 'html-v1');
484
- await this.deliverReadyEntry(ref);
646
+ if (durable?.state === 'processing' && queuedClaim !== ref) {
647
+ await this.interruptResponse(ref);
485
648
  return;
486
649
  }
487
650
  const selected = (0, telegram_text_entities_js_1.selectTelegramTextEntities)(message);
@@ -489,13 +652,23 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
489
652
  if (!selected.text.trim() && files.length === 0)
490
653
  return;
491
654
  const formatting = (0, telegram_text_entities_js_1.captureTelegramTextFormatting)(selected.field, selected.text, selected.entities);
492
- const inputIdentity = (0, node_crypto_1.createHash)('sha256')
493
- .update(`${selected.text}\0${files.map(({ file }) => file.file_unique_id).join(',')}`)
494
- .digest('hex');
495
- const ledgerEntry = this.messageLedger.claim(ref, {
496
- deliveryTarget: `telegram:${chatId}`,
497
- payloadIdentity: inputIdentity,
498
- }).entry;
655
+ const memberRoom = !privateChat &&
656
+ (durable ? durable.principalId !== undefined : !(0, owner_authority_js_1.holdsOwnerRole)(this.registry, principalId));
657
+ const ledgerEntry = this.claimMessage(message, privateChat || memberRoom ? principalId : undefined).entry;
658
+ if (intake.deferIncoming?.()) {
659
+ intake.acceptOwnerMessage({
660
+ id: ref,
661
+ channelKey: chatId,
662
+ senderPrincipalId: principalId,
663
+ occurredAt: message.date * 1000,
664
+ text: selected.text,
665
+ payload: { pendingTelegramMessage: JSON.parse(JSON.stringify(message)) },
666
+ });
667
+ const pendingPresenter = this.createResponsePresenter(ref, Number(message.chat.id));
668
+ this.activePresenters.set(ref, pendingPresenter);
669
+ await pendingPresenter.start();
670
+ return;
671
+ }
499
672
  const presenter = this.createResponsePresenter(ref, Number(message.chat.id));
500
673
  this.activePresenters.set(ref, presenter);
501
674
  try {
@@ -504,12 +677,16 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
504
677
  const attachments = await (0, telegram_attachments_js_1.downloadTelegramFiles)(files, {
505
678
  api: this.bot.api,
506
679
  token: this.token,
507
- downloadsDir: this.downloadsDir,
680
+ downloadsDir: privateChat || memberRoom
681
+ ? this.principalPaths(principal.principalId, memberRoom ? chatId : undefined)
682
+ .downloadsDir
683
+ : this.downloadsDir,
508
684
  messageId: message.message_id,
509
685
  });
510
686
  const input = {
511
687
  id: ref,
512
688
  channelKey: chatId,
689
+ senderPrincipalId: principal.principalId,
513
690
  occurredAt: message.date * 1000,
514
691
  text: selected.text.trim()
515
692
  ? selected.text
@@ -523,7 +700,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
523
700
  },
524
701
  }),
525
702
  };
526
- this.intake.acceptOwnerMessage(input);
703
+ intake.acceptOwnerMessage(input);
527
704
  this.emitEvent({
528
705
  type: 'message_received',
529
706
  source: 'telegram',
@@ -536,6 +713,48 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
536
713
  throw error;
537
714
  }
538
715
  }
716
+ /** Host preparation of a transport-verified envelope accepted while the member was retired. */
717
+ async prepareDeferredMemberInput(principalId, input) {
718
+ const payload = input.payload;
719
+ const message = payload.pendingTelegramMessage;
720
+ const person = this.registry.resolveByExternal('telegram', 'private', String(message.from.id));
721
+ if (person?.principalId !== principalId ||
722
+ person.status !== 'active' ||
723
+ sourceMessageRef(String(message.chat.id), message.message_id) !== input.id)
724
+ throw new Error('Deferred Telegram input does not belong to this active person');
725
+ const selected = (0, telegram_text_entities_js_1.selectTelegramTextEntities)(message);
726
+ const formatting = (0, telegram_text_entities_js_1.captureTelegramTextFormatting)(selected.field, selected.text, selected.entities);
727
+ const attachments = await (0, telegram_attachments_js_1.downloadTelegramFiles)((0, telegram_attachments_js_1.telegramFiles)(message), {
728
+ api: this.bot.api,
729
+ token: this.token,
730
+ // A member's room message downloads into that room session's paths.
731
+ downloadsDir: this.principalPaths(principalId, message.chat.type === 'private' ? undefined : String(message.chat.id)).downloadsDir,
732
+ messageId: message.message_id,
733
+ });
734
+ return {
735
+ ...input,
736
+ text: selected.text.trim()
737
+ ? selected.text
738
+ : attachments.map(({ name }) => `[file: ${name}]`).join('\n'),
739
+ payload: {
740
+ ...(formatting === undefined ? {} : { telegramFormatting: formatting }),
741
+ ...(attachments.length ? { attachments } : {}),
742
+ },
743
+ };
744
+ }
745
+ claimMessage(message, principalId) {
746
+ const selected = (0, telegram_text_entities_js_1.selectTelegramTextEntities)(message);
747
+ const files = (0, telegram_attachments_js_1.telegramFiles)(message);
748
+ const inputIdentity = (0, node_crypto_1.createHash)('sha256')
749
+ .update(`${selected.text}\0${files.map(({ file }) => file.file_unique_id).join(',')}`)
750
+ .digest('hex');
751
+ const chatId = String(message.chat.id);
752
+ return this.messageLedger.claim(sourceMessageRef(chatId, message.message_id), {
753
+ deliveryTarget: `telegram:${chatId}`,
754
+ payloadIdentity: inputIdentity,
755
+ ...(principalId === undefined ? {} : { principalId }),
756
+ });
757
+ }
539
758
  createResponsePresenter(sourceRef, chatId) {
540
759
  if (!this.bot)
541
760
  throw new Error('Telegram gateway not connected');
@@ -571,6 +790,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
571
790
  return;
572
791
  if (current.deliveryUncertain)
573
792
  throw new Error('Telegram response delivery is uncertain');
793
+ const intake = await this.responseIntake(sourceRef);
574
794
  const presenter = this.activePresenters.get(sourceRef) ?? this.createResponsePresenter(sourceRef, chatId);
575
795
  try {
576
796
  await presenter.finalize(current.response);
@@ -583,8 +803,13 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
583
803
  if (current.responseAuthor === undefined) {
584
804
  this.log(`telegram reply archive skipped key=${sourceRef} reason=missing_response_author`);
585
805
  }
806
+ else if (current.responseAuthor === 'host' &&
807
+ intake.hasOwnerMessage?.(sourceRef) === false) {
808
+ // An intake interrupted before storage has no original to attach its notice to.
809
+ this.log(`telegram reply archive skipped key=${sourceRef} reason=missing_owner_message`);
810
+ }
586
811
  else {
587
- this.intake.recordOwnerReply({
812
+ intake.recordOwnerReply({
588
813
  messageRef: sourceRef,
589
814
  text: current.response,
590
815
  occurredAt: this.messageLedger.get(sourceRef).updatedAt,
@@ -600,7 +825,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
600
825
  timestamp: new Date(),
601
826
  data: { sourceMessageRef: sourceRef },
602
827
  });
603
- });
828
+ }, true);
604
829
  }
605
830
  async recoverPendingResponses() {
606
831
  for (const entry of this.messageLedger.listUndelivered()) {
@@ -610,27 +835,31 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
610
835
  continue;
611
836
  }
612
837
  if (entry.key.startsWith('outbound:') && entry.state === 'ready') {
838
+ // Personal turn recovery belongs to mailbox reconciliation, which checks the current person
839
+ // and input before sending. Gateway startup cannot establish that authority.
840
+ if (entry.idempotencyKey && (0, personal_turns_js_1.isPersonalTurnTextKey)(entry.idempotencyKey))
841
+ continue;
613
842
  const chatId = entry.deliveryTarget?.slice('telegram:'.length);
614
843
  if (!entry.deliveryTarget?.startsWith('telegram:') || !chatId) {
615
844
  throw new Error('Telegram outbound entry has no valid destination');
616
845
  }
617
- if (!this.config.allowedChats?.includes(chatId))
618
- continue;
846
+ if (!this.config.allowedChats?.includes(chatId)) {
847
+ const recipient = this.registry.resolveByExternal('telegram', 'private', chatId);
848
+ if (recipient?.kind !== 'member' || recipient.status !== 'active')
849
+ continue;
850
+ }
619
851
  await this.runInChatQueue(chatId, () => this.deliverOutboundEntry(entry.key, chatId));
620
852
  continue;
621
853
  }
622
854
  if (!entry.key.startsWith('telegram:'))
623
855
  continue;
624
- const chatId = chatIdFromSourceMessageRef(entry.key);
625
- if (!this.config.allowedChats?.includes(chatId))
626
- continue;
856
+ const intake = await this.responseIntake(entry.key);
627
857
  if (entry.state === 'ready' && entry.response !== undefined) {
628
858
  await this.deliverReadyEntry(entry.key);
629
859
  continue;
630
860
  }
631
- if (entry.state === 'processing' && !this.intake.isPending?.(entry.key)) {
632
- this.messageLedger.markInterrupted(entry.key, this.interruptedNotice, 'html-v1');
633
- await this.deliverReadyEntry(entry.key);
861
+ if (entry.state === 'processing' && !intake.isPending?.(entry.key)) {
862
+ await this.interruptResponse(entry.key);
634
863
  }
635
864
  }
636
865
  catch (error) {
@@ -648,7 +877,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
648
877
  await sendFormattedMessage(this.bot.api, Number(chatId), chunk);
649
878
  return;
650
879
  }
651
- const key = outboundLedgerKey(idempotencyKey);
880
+ const key = (0, telegram_message_ledger_js_1.outboundLedgerKey)(idempotencyKey);
652
881
  const binding = {
653
882
  deliveryTarget: `telegram:${chatId}`,
654
883
  payloadIdentity: (0, node_crypto_1.createHash)('sha256').update(text).digest('hex'),
@@ -658,6 +887,9 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
658
887
  const existing = this.messageLedger.claim(key, binding).entry;
659
888
  if (existing.state === 'delivered')
660
889
  return;
890
+ // Keep the rejection until the occurrence closes. A restart in between must not send it.
891
+ if ((0, personal_turns_js_1.isPersonalTurnTextKey)(idempotencyKey) && existing.state === 'failed')
892
+ throw new telegram_errors_js_1.TelegramJobDeliveryRejected('Personal job DM delivery was already rejected');
661
893
  if (existing.state !== 'ready')
662
894
  this.messageLedger.markReady(key, text, 'html-v1');
663
895
  await this.deliverOutboundEntry(key, chatId);
@@ -688,7 +920,11 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
688
920
  }
689
921
  catch (error) {
690
922
  if ((0, telegram_errors_js_1.isDefinitiveTelegramRejection)(error)) {
691
- this.messageLedger.markDeliveryProgress(key, index, false);
923
+ // A personal turn refusal is final. Never persist a retryable ready receipt before its failure.
924
+ if (entry.idempotencyKey && (0, personal_turns_js_1.isPersonalTurnTextKey)(entry.idempotencyKey))
925
+ this.messageLedger.markFailed(key);
926
+ else
927
+ this.messageLedger.markDeliveryProgress(key, index, false);
692
928
  }
693
929
  throw error;
694
930
  }
@@ -707,12 +943,14 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
707
943
  });
708
944
  const current = (previous ?? Promise.resolve()).catch(() => { }).then(() => gate);
709
945
  this.chatTails.set(chatId, current);
946
+ const context = { chatId, active: true };
710
947
  try {
711
948
  if (previous)
712
949
  await previous.catch(() => { });
713
- return await this.activeChat.run({ chatId, active: true }, work);
950
+ return await this.activeChat.run(context, work);
714
951
  }
715
952
  finally {
953
+ context.active = false;
716
954
  release();
717
955
  if (this.chatTails.get(chatId) === current)
718
956
  this.chatTails.delete(chatId);