@jungjaehoon/mama-os 0.75.0 → 0.77.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 (260) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +1 -1
  3. package/dist/api/attachment-actions.d.ts.map +1 -1
  4. package/dist/api/attachment-actions.js +8 -7
  5. package/dist/api/attachment-actions.js.map +1 -1
  6. package/dist/api/code-act-actions.d.ts +3 -1
  7. package/dist/api/code-act-actions.d.ts.map +1 -1
  8. package/dist/api/code-act-actions.js +21 -9
  9. package/dist/api/code-act-actions.js.map +1 -1
  10. package/dist/api/document-actions.d.ts +13 -0
  11. package/dist/api/document-actions.d.ts.map +1 -0
  12. package/dist/api/document-actions.js +55 -0
  13. package/dist/api/document-actions.js.map +1 -0
  14. package/dist/api/drive-actions.d.ts +4 -0
  15. package/dist/api/drive-actions.d.ts.map +1 -1
  16. package/dist/api/drive-actions.js +15 -7
  17. package/dist/api/drive-actions.js.map +1 -1
  18. package/dist/api/drive-delivery.d.ts.map +1 -1
  19. package/dist/api/drive-delivery.js +8 -3
  20. package/dist/api/drive-delivery.js.map +1 -1
  21. package/dist/api/member-enrollment.d.ts +9 -3
  22. package/dist/api/member-enrollment.d.ts.map +1 -1
  23. package/dist/api/member-enrollment.js +64 -16
  24. package/dist/api/member-enrollment.js.map +1 -1
  25. package/dist/api/member-export.d.ts.map +1 -1
  26. package/dist/api/member-export.js +14 -6
  27. package/dist/api/member-export.js.map +1 -1
  28. package/dist/api/member-lifecycle.d.ts +25 -4
  29. package/dist/api/member-lifecycle.d.ts.map +1 -1
  30. package/dist/api/member-lifecycle.js +88 -21
  31. package/dist/api/member-lifecycle.js.map +1 -1
  32. package/dist/api/member-notice-actions.d.ts +18 -0
  33. package/dist/api/member-notice-actions.d.ts.map +1 -0
  34. package/dist/api/member-notice-actions.js +184 -0
  35. package/dist/api/member-notice-actions.js.map +1 -0
  36. package/dist/api/member-records.d.ts +17 -2
  37. package/dist/api/member-records.d.ts.map +1 -1
  38. package/dist/api/member-records.js +440 -82
  39. package/dist/api/member-records.js.map +1 -1
  40. package/dist/api/member-share-actions.d.ts +3 -3
  41. package/dist/api/member-share-actions.d.ts.map +1 -1
  42. package/dist/api/member-share-actions.js +47 -20
  43. package/dist/api/member-share-actions.js.map +1 -1
  44. package/dist/api/owner-policy-actions.d.ts +1 -0
  45. package/dist/api/owner-policy-actions.d.ts.map +1 -1
  46. package/dist/api/owner-policy-actions.js +8 -4
  47. package/dist/api/owner-policy-actions.js.map +1 -1
  48. package/dist/api/owner-timezone-actions.d.ts +5 -2
  49. package/dist/api/owner-timezone-actions.d.ts.map +1 -1
  50. package/dist/api/owner-timezone-actions.js +4 -6
  51. package/dist/api/owner-timezone-actions.js.map +1 -1
  52. package/dist/api/personal-job-actions.d.ts +6 -0
  53. package/dist/api/personal-job-actions.d.ts.map +1 -0
  54. package/dist/api/personal-job-actions.js +72 -0
  55. package/dist/api/personal-job-actions.js.map +1 -0
  56. package/dist/api/security-events.d.ts +6 -1
  57. package/dist/api/security-events.d.ts.map +1 -1
  58. package/dist/api/security-events.js +34 -15
  59. package/dist/api/security-events.js.map +1 -1
  60. package/dist/api/skill-actions.d.ts +5 -0
  61. package/dist/api/skill-actions.d.ts.map +1 -0
  62. package/dist/api/skill-actions.js +66 -0
  63. package/dist/api/skill-actions.js.map +1 -0
  64. package/dist/api/source-actions.d.ts.map +1 -1
  65. package/dist/api/source-actions.js +4 -2
  66. package/dist/api/source-actions.js.map +1 -1
  67. package/dist/api/stored-source-reader.d.ts.map +1 -1
  68. package/dist/api/stored-source-reader.js +9 -2
  69. package/dist/api/stored-source-reader.js.map +1 -1
  70. package/dist/api/trello-actions.d.ts.map +1 -1
  71. package/dist/api/trello-actions.js +8 -2
  72. package/dist/api/trello-actions.js.map +1 -1
  73. package/dist/api/work-actions.d.ts +12 -2
  74. package/dist/api/work-actions.d.ts.map +1 -1
  75. package/dist/api/work-actions.js +57 -10
  76. package/dist/api/work-actions.js.map +1 -1
  77. package/dist/cli/commands/daemon.d.ts +12 -2
  78. package/dist/cli/commands/daemon.d.ts.map +1 -1
  79. package/dist/cli/commands/daemon.js +85 -13
  80. package/dist/cli/commands/daemon.js.map +1 -1
  81. package/dist/cli/commands/init.d.ts +4 -0
  82. package/dist/cli/commands/init.d.ts.map +1 -1
  83. package/dist/cli/commands/init.js +23 -0
  84. package/dist/cli/commands/init.js.map +1 -1
  85. package/dist/cli/commands/tools.d.ts +12 -0
  86. package/dist/cli/commands/tools.d.ts.map +1 -0
  87. package/dist/cli/commands/tools.js +109 -0
  88. package/dist/cli/commands/tools.js.map +1 -0
  89. package/dist/cli/index.js +6 -1
  90. package/dist/cli/index.js.map +1 -1
  91. package/dist/cli/runtime/claude-caller-config.d.ts +3 -1
  92. package/dist/cli/runtime/claude-caller-config.d.ts.map +1 -1
  93. package/dist/cli/runtime/claude-caller-config.js +14 -2
  94. package/dist/cli/runtime/claude-caller-config.js.map +1 -1
  95. package/dist/connectors/framework/polling-scheduler.d.ts +6 -1
  96. package/dist/connectors/framework/polling-scheduler.d.ts.map +1 -1
  97. package/dist/connectors/framework/polling-scheduler.js +25 -3
  98. package/dist/connectors/framework/polling-scheduler.js.map +1 -1
  99. package/dist/connectors/framework/stored-index-read.d.ts +2 -2
  100. package/dist/connectors/framework/stored-index-read.d.ts.map +1 -1
  101. package/dist/connectors/framework/stored-index-read.js +10 -3
  102. package/dist/connectors/framework/stored-index-read.js.map +1 -1
  103. package/dist/gateways/base-gateway.d.ts +7 -0
  104. package/dist/gateways/base-gateway.d.ts.map +1 -1
  105. package/dist/gateways/base-gateway.js +24 -0
  106. package/dist/gateways/base-gateway.js.map +1 -1
  107. package/dist/gateways/discord.d.ts +4 -0
  108. package/dist/gateways/discord.d.ts.map +1 -1
  109. package/dist/gateways/discord.js +24 -6
  110. package/dist/gateways/discord.js.map +1 -1
  111. package/dist/gateways/slack.d.ts +3 -0
  112. package/dist/gateways/slack.d.ts.map +1 -1
  113. package/dist/gateways/slack.js +15 -2
  114. package/dist/gateways/slack.js.map +1 -1
  115. package/dist/gateways/telegram-errors.d.ts +3 -0
  116. package/dist/gateways/telegram-errors.d.ts.map +1 -1
  117. package/dist/gateways/telegram-errors.js +5 -0
  118. package/dist/gateways/telegram-errors.js.map +1 -1
  119. package/dist/gateways/telegram-message-ledger.d.ts +7 -0
  120. package/dist/gateways/telegram-message-ledger.d.ts.map +1 -1
  121. package/dist/gateways/telegram-message-ledger.js +27 -2
  122. package/dist/gateways/telegram-message-ledger.js.map +1 -1
  123. package/dist/gateways/telegram.d.ts +24 -6
  124. package/dist/gateways/telegram.d.ts.map +1 -1
  125. package/dist/gateways/telegram.js +258 -44
  126. package/dist/gateways/telegram.js.map +1 -1
  127. package/dist/gateways/turn-contract.d.ts +6 -0
  128. package/dist/gateways/turn-contract.d.ts.map +1 -1
  129. package/dist/runtime/action-surface.d.ts +28 -3
  130. package/dist/runtime/action-surface.d.ts.map +1 -1
  131. package/dist/runtime/action-surface.js +316 -34
  132. package/dist/runtime/action-surface.js.map +1 -1
  133. package/dist/runtime/backend-security.d.ts +4 -0
  134. package/dist/runtime/backend-security.d.ts.map +1 -1
  135. package/dist/runtime/backend-security.js +8 -1
  136. package/dist/runtime/backend-security.js.map +1 -1
  137. package/dist/runtime/claude-caller-hook.d.ts +14 -0
  138. package/dist/runtime/claude-caller-hook.d.ts.map +1 -1
  139. package/dist/runtime/claude-caller-hook.js +21 -1
  140. package/dist/runtime/claude-caller-hook.js.map +1 -1
  141. package/dist/runtime/config.d.ts +6 -0
  142. package/dist/runtime/config.d.ts.map +1 -1
  143. package/dist/runtime/config.js +24 -0
  144. package/dist/runtime/config.js.map +1 -1
  145. package/dist/runtime/connectors.js +1 -1
  146. package/dist/runtime/connectors.js.map +1 -1
  147. package/dist/runtime/egress-proxy.d.ts +3 -3
  148. package/dist/runtime/egress-proxy.d.ts.map +1 -1
  149. package/dist/runtime/egress-proxy.js +16 -3
  150. package/dist/runtime/egress-proxy.js.map +1 -1
  151. package/dist/runtime/member-paths.d.ts +9 -1
  152. package/dist/runtime/member-paths.d.ts.map +1 -1
  153. package/dist/runtime/member-paths.js +67 -5
  154. package/dist/runtime/member-paths.js.map +1 -1
  155. package/dist/runtime/member-session.d.ts +21 -21
  156. package/dist/runtime/member-session.d.ts.map +1 -1
  157. package/dist/runtime/member-session.js +297 -34
  158. package/dist/runtime/member-session.js.map +1 -1
  159. package/dist/runtime/member-system-prompt.d.ts +3 -0
  160. package/dist/runtime/member-system-prompt.d.ts.map +1 -1
  161. package/dist/runtime/member-system-prompt.js +30 -4
  162. package/dist/runtime/member-system-prompt.js.map +1 -1
  163. package/dist/runtime/native-session.d.ts +8 -0
  164. package/dist/runtime/native-session.d.ts.map +1 -1
  165. package/dist/runtime/native-session.js +105 -17
  166. package/dist/runtime/native-session.js.map +1 -1
  167. package/dist/runtime/outbound-attempts.d.ts +5 -2
  168. package/dist/runtime/outbound-attempts.d.ts.map +1 -1
  169. package/dist/runtime/outbound-attempts.js +3 -2
  170. package/dist/runtime/outbound-attempts.js.map +1 -1
  171. package/dist/runtime/owner-authority.d.ts +16 -14
  172. package/dist/runtime/owner-authority.d.ts.map +1 -1
  173. package/dist/runtime/owner-authority.js +163 -7
  174. package/dist/runtime/owner-authority.js.map +1 -1
  175. package/dist/runtime/owner-runtime.d.ts +18 -5
  176. package/dist/runtime/owner-runtime.d.ts.map +1 -1
  177. package/dist/runtime/owner-runtime.js +429 -43
  178. package/dist/runtime/owner-runtime.js.map +1 -1
  179. package/dist/runtime/owner-system-prompt.d.ts +3 -1
  180. package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
  181. package/dist/runtime/owner-system-prompt.js +16 -6
  182. package/dist/runtime/owner-system-prompt.js.map +1 -1
  183. package/dist/runtime/personal-job-cron.d.ts +6 -0
  184. package/dist/runtime/personal-job-cron.d.ts.map +1 -0
  185. package/dist/runtime/personal-job-cron.js +154 -0
  186. package/dist/runtime/personal-job-cron.js.map +1 -0
  187. package/dist/runtime/personal-jobs.d.ts +107 -0
  188. package/dist/runtime/personal-jobs.d.ts.map +1 -0
  189. package/dist/runtime/personal-jobs.js +353 -0
  190. package/dist/runtime/personal-jobs.js.map +1 -0
  191. package/dist/runtime/personal-notices.d.ts +38 -0
  192. package/dist/runtime/personal-notices.d.ts.map +1 -0
  193. package/dist/runtime/personal-notices.js +137 -0
  194. package/dist/runtime/personal-notices.js.map +1 -0
  195. package/dist/runtime/personal-turns.d.ts +23 -0
  196. package/dist/runtime/personal-turns.d.ts.map +1 -0
  197. package/dist/runtime/personal-turns.js +51 -0
  198. package/dist/runtime/personal-turns.js.map +1 -0
  199. package/dist/runtime/principal-access.d.ts +8 -0
  200. package/dist/runtime/principal-access.d.ts.map +1 -1
  201. package/dist/runtime/principal-access.js +70 -2
  202. package/dist/runtime/principal-access.js.map +1 -1
  203. package/dist/runtime/principal-sessions.d.ts +16 -2
  204. package/dist/runtime/principal-sessions.d.ts.map +1 -1
  205. package/dist/runtime/principal-sessions.js +104 -21
  206. package/dist/runtime/principal-sessions.js.map +1 -1
  207. package/dist/runtime/skills.d.ts +86 -0
  208. package/dist/runtime/skills.d.ts.map +1 -0
  209. package/dist/runtime/skills.js +560 -0
  210. package/dist/runtime/skills.js.map +1 -0
  211. package/dist/runtime/stimulus-delivery.d.ts +21 -1
  212. package/dist/runtime/stimulus-delivery.d.ts.map +1 -1
  213. package/dist/runtime/stimulus-delivery.js +185 -18
  214. package/dist/runtime/stimulus-delivery.js.map +1 -1
  215. package/dist/runtime/tools-paths.d.ts +9 -0
  216. package/dist/runtime/tools-paths.d.ts.map +1 -0
  217. package/dist/runtime/tools-paths.js +98 -0
  218. package/dist/runtime/tools-paths.js.map +1 -0
  219. package/dist/runtime/turn-orders.d.ts +22 -2
  220. package/dist/runtime/turn-orders.d.ts.map +1 -1
  221. package/dist/runtime/turn-orders.js +60 -6
  222. package/dist/runtime/turn-orders.js.map +1 -1
  223. package/dist/runtime/web-fetch-scope.d.ts +20 -0
  224. package/dist/runtime/web-fetch-scope.d.ts.map +1 -0
  225. package/dist/runtime/web-fetch-scope.js +68 -0
  226. package/dist/runtime/web-fetch-scope.js.map +1 -0
  227. package/dist/storage/chat-sources.d.ts +21 -0
  228. package/dist/storage/chat-sources.d.ts.map +1 -1
  229. package/dist/storage/chat-sources.js +65 -3
  230. package/dist/storage/chat-sources.js.map +1 -1
  231. package/dist/tools/command-runner.d.ts +12 -0
  232. package/dist/tools/command-runner.d.ts.map +1 -0
  233. package/dist/tools/command-runner.js +65 -0
  234. package/dist/tools/command-runner.js.map +1 -0
  235. package/dist/tools/document-render.d.ts +24 -0
  236. package/dist/tools/document-render.d.ts.map +1 -0
  237. package/dist/tools/document-render.js +208 -0
  238. package/dist/tools/document-render.js.map +1 -0
  239. package/dist/tools/document-toolkit.d.ts +15 -0
  240. package/dist/tools/document-toolkit.d.ts.map +1 -0
  241. package/dist/tools/document-toolkit.js +501 -0
  242. package/dist/tools/document-toolkit.js.map +1 -0
  243. package/dist/tools/office-render.d.ts +28 -0
  244. package/dist/tools/office-render.d.ts.map +1 -0
  245. package/dist/tools/office-render.js +101 -0
  246. package/dist/tools/office-render.js.map +1 -0
  247. package/dist/tools/toolkit-process.d.ts +9 -0
  248. package/dist/tools/toolkit-process.d.ts.map +1 -0
  249. package/dist/tools/toolkit-process.js +148 -0
  250. package/dist/tools/toolkit-process.js.map +1 -0
  251. package/dist/tools/toolkit-state.d.ts +22 -0
  252. package/dist/tools/toolkit-state.d.ts.map +1 -0
  253. package/dist/tools/toolkit-state.js +116 -0
  254. package/dist/tools/toolkit-state.js.map +1 -0
  255. package/package.json +3 -2
  256. package/toolkit/inspect-office-zip.py +16 -0
  257. package/toolkit/rasterize.py +29 -0
  258. package/toolkit/requirements.in +11 -0
  259. package/toolkit/requirements.lock +1643 -0
  260. package/toolkit/smoke.py +32 -0
@@ -1,9 +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");
6
7
  const owner_authority_js_1 = require("../runtime/owner-authority.js");
8
+ const principal_access_js_1 = require("../runtime/principal-access.js");
7
9
  const node_fs_1 = require("node:fs");
8
10
  const node_path_1 = require("node:path");
9
11
  const grammy_1 = require("grammy");
@@ -66,9 +68,6 @@ function chatIdFromSourceMessageRef(value) {
66
68
  }
67
69
  return rest.slice(0, separator);
68
70
  }
69
- function outboundLedgerKey(idempotencyKey) {
70
- return `outbound:${(0, node_crypto_1.createHash)('sha256').update(`text\0${idempotencyKey}`).digest('hex')}`;
71
- }
72
71
  /** Telegram personal/common ingress and its durable response transport. */
73
72
  class TelegramGateway extends base_gateway_js_1.BaseGateway {
74
73
  source = 'telegram';
@@ -84,11 +83,17 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
84
83
  onFatalError;
85
84
  messageLedger;
86
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();
87
92
  activePresenters = new Map();
88
93
  recentMessageIds = new Map();
89
94
  activeChat = new node_async_hooks_1.AsyncLocalStorage();
90
95
  nextEnrollmentRequestId = (0, node_crypto_1.randomInt)(1, 0x7fffffff);
91
- pendingEnrollment;
96
+ pendingEnrollments = new Map();
92
97
  onMemberSelection;
93
98
  recordMemberSelection;
94
99
  bot = null;
@@ -134,6 +139,10 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
134
139
  async start() {
135
140
  if (this.connected)
136
141
  return;
142
+ this.accepting = true;
143
+ this.intakeStopped = new Promise((resolve) => {
144
+ this.stopIntake = () => resolve(undefined);
145
+ });
137
146
  if (!this.config.allowedChats?.some((chatId) => chatId.trim().length > 0)) {
138
147
  throw new Error('telegram gateway disabled: allowed_chats is not set. Run: mama status');
139
148
  }
@@ -178,10 +187,21 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
178
187
  throw error;
179
188
  }
180
189
  }
181
- async stop() {
182
- this.pendingEnrollment = undefined;
190
+ async beginStop() {
191
+ this.accepting = false;
192
+ this.stopIntake();
183
193
  if (this.bot)
184
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();
185
205
  this.bot = null;
186
206
  this.connected = false;
187
207
  this.recentMessageIds.clear();
@@ -202,7 +222,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
202
222
  }
203
223
  if (existing.state === 'delivered')
204
224
  return;
205
- this.responseIntake(sourceRef);
225
+ await this.responseIntake(sourceRef);
206
226
  if (existing.state === 'ready' && existing.response !== response) {
207
227
  throw new Error('Telegram response conflicts with its durable ledger entry');
208
228
  }
@@ -212,6 +232,18 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
212
232
  return;
213
233
  await this.deliverReadyEntry(sourceRef);
214
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
+ }
215
247
  async sendMessage(chatId, text, idempotencyKey) {
216
248
  if (!this.bot || !this.connected)
217
249
  throw new Error('Telegram gateway not connected');
@@ -228,6 +260,19 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
228
260
  await this.sendMessage(ownerChatId, text, idempotencyKey);
229
261
  }
230
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
+ }
231
276
  async sendMemberText(access, text, operationId) {
232
277
  const destinations = access.destinations?.filter((target) => target.kind === 'telegram');
233
278
  if (destinations?.length !== 1 || !destinations[0].id.trim())
@@ -238,9 +283,12 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
238
283
  throw new Error('Member text operation id is required');
239
284
  const chatId = destinations[0].id;
240
285
  await this.runInChatQueue(chatId, () => this.sendMessageNow(chatId, text, operationId));
241
- return this.messageLedger.get(outboundLedgerKey(operationId))?.state === 'delivered';
286
+ return this.memberTextDelivered(access, operationId);
242
287
  }
243
- async sendFile(path, caption, operationId, member) {
288
+ sendFile(path, caption, operationId, member) {
289
+ return this.trackDelivery(() => this.sendFileNow(path, caption, operationId, member));
290
+ }
291
+ async sendFileNow(path, caption, operationId, member) {
244
292
  if (!this.bot || !this.connected)
245
293
  throw new Error('Telegram gateway not connected');
246
294
  if (operationId.trim() === '')
@@ -253,6 +301,8 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
253
301
  if (destinations?.length !== 1 || !destinations[0].id.trim())
254
302
  throw new Error('Member file delivery requires exactly one Telegram destination');
255
303
  chatId = destinations[0].id;
304
+ if ((0, principal_access_js_1.roomChannel)(member.access) !== undefined)
305
+ this.requireAllowedChat(chatId);
256
306
  filesRoot = member.filesRoot;
257
307
  }
258
308
  else {
@@ -304,12 +354,12 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
304
354
  }
305
355
  }
306
356
  /** The source ref was verified as an owner turn by the runtime; only an owner DM can select. */
307
- async requestMemberEnrollment(sourceRef) {
357
+ async requestMemberEnrollment(sourceRef, requestingOwnerUserId, name) {
308
358
  if (!sourceRef.startsWith('telegram:'))
309
359
  throw Object.assign(new Error("Enrollment requires the owner's own Telegram DM"), {
310
360
  name: 'denied',
311
361
  });
312
- const ownerUserId = chatIdFromSourceMessageRef(sourceRef);
362
+ const ownerUserId = requestingOwnerUserId;
313
363
  const chatId = Number(ownerUserId);
314
364
  if (!this.bot || !this.onMemberSelection || !this.ownerAllowed(ownerUserId)) {
315
365
  throw Object.assign(new Error("Enrollment requires the owner's own Telegram DM"), {
@@ -318,7 +368,12 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
318
368
  }
319
369
  const requestId = this.nextEnrollmentRequestId;
320
370
  this.nextEnrollmentRequestId = requestId === 0x7fffffff ? 1 : requestId + 1;
321
- this.pendingEnrollment = { requestId, sourceMessageRef: sourceRef, ownerUserId };
371
+ this.pendingEnrollments.set(ownerUserId, {
372
+ requestId,
373
+ sourceMessageRef: sourceRef,
374
+ ownerUserId,
375
+ ...(name === undefined ? {} : { name }),
376
+ });
322
377
  // Telegram shows a request_users button only on the reply keyboard, under the message box.
323
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.';
324
379
  try {
@@ -338,15 +393,24 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
338
393
  });
339
394
  }
340
395
  catch (error) {
341
- if (this.pendingEnrollment?.requestId === requestId)
342
- this.pendingEnrollment = undefined;
396
+ if (this.pendingEnrollments.get(ownerUserId)?.requestId === requestId)
397
+ this.pendingEnrollments.delete(ownerUserId);
343
398
  throw error;
344
399
  }
345
400
  }
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
+ }
346
408
  async completeMemberSelection(message, senderPrincipalId) {
347
409
  const chatId = String(message.chat.id);
348
410
  const ownerUserId = String(message.from.id);
349
- if (message.chat.type !== 'private' || chatId !== ownerUserId)
411
+ if (message.chat.type !== 'private' ||
412
+ chatId !== ownerUserId ||
413
+ !this.ownerAllowed(ownerUserId))
350
414
  return;
351
415
  let receiptText = 'Enrollment selection received.';
352
416
  let deliveryVerified = false;
@@ -359,7 +423,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
359
423
  deliveryVerified = true;
360
424
  };
361
425
  try {
362
- const pending = this.pendingEnrollment;
426
+ const pending = this.pendingEnrollments.get(ownerUserId);
363
427
  const shared = message.users_shared;
364
428
  if (!pending ||
365
429
  shared.request_id !== pending.requestId ||
@@ -368,7 +432,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
368
432
  return;
369
433
  }
370
434
  // Consume before awaiting the serial host chain, so concurrent duplicate updates cannot enroll.
371
- this.pendingEnrollment = undefined;
435
+ this.pendingEnrollments.delete(ownerUserId);
372
436
  if (shared.users.length !== 1 ||
373
437
  !Number.isSafeInteger(shared.users[0].user_id) ||
374
438
  shared.users[0].user_id <= 0) {
@@ -382,6 +446,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
382
446
  sourceMessageRef: pending.sourceMessageRef,
383
447
  ownerUserId,
384
448
  userId: String(shared.users[0].user_id),
449
+ ...(pending.name === undefined ? {} : { name: pending.name }),
385
450
  });
386
451
  }
387
452
  catch (error) {
@@ -442,7 +507,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
442
507
  principal.status === 'active' &&
443
508
  (0, owner_authority_js_1.holdsOwnerRole)(this.registry, principal.principalId));
444
509
  }
445
- responseIntake(sourceRef) {
510
+ async responseIntake(sourceRef) {
446
511
  const chatId = chatIdFromSourceMessageRef(sourceRef);
447
512
  const recipient = this.registry.resolveByExternal('telegram', 'private', chatId);
448
513
  if (recipient) {
@@ -452,6 +517,15 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
452
517
  return this.memberIntake(recipient.principalId);
453
518
  }
454
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
+ }
455
529
  return this.intake;
456
530
  }
457
531
  requireAllowedChat(chatId) {
@@ -459,7 +533,10 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
459
533
  throw new Error('Telegram destination is not allowlisted');
460
534
  }
461
535
  }
462
- async handleMessage(message) {
536
+ handleMessage(message) {
537
+ return this.trackIntake(() => this.handleMessageInput(message));
538
+ }
539
+ async handleMessageInput(message) {
463
540
  if (!message.chat || !message.from)
464
541
  return;
465
542
  const chatId = String(message.chat.id);
@@ -468,19 +545,84 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
468
545
  const privateChat = message.chat.type === 'private';
469
546
  const admitted = principal?.kind === 'member' &&
470
547
  principal.status === 'active' &&
471
- (privateChat || (this.config.allowedChats?.includes(chatId) && this.ownerAllowed(userId)));
548
+ (privateChat || this.config.allowedChats?.includes(chatId));
472
549
  if (message.from.is_bot || !admitted) {
550
+ if (principal?.status === 'suspended')
551
+ this.cancelMemberEnrollment(principal.principalId);
473
552
  const chatHash = (0, node_crypto_1.createHash)('sha256').update(`chat\0${chatId}`).digest('hex');
474
553
  const senderHash = (0, node_crypto_1.createHash)('sha256').update(`sender\0${userId}`).digest('hex');
475
554
  console.warn(`telegram message dropped reason=non_owner chat_hash=${chatHash} sender_hash=${senderHash}`);
476
555
  return;
477
556
  }
478
557
  if (message.users_shared) {
479
- await this.completeMemberSelection(message, principal.principalId);
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}`);
480
623
  return;
481
624
  }
482
625
  const ref = sourceMessageRef(chatId, message.message_id);
483
- const intake = privateChat ? this.memberIntake(principal.principalId) : this.intake;
484
626
  const now = Date.now();
485
627
  const previous = this.recentMessageIds.get(ref);
486
628
  if (previous !== undefined && now - previous <= MESSAGE_DEDUP_TTL_MS)
@@ -501,9 +643,8 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
501
643
  return;
502
644
  if (durable?.state === 'processing' && intake.isPending?.(ref))
503
645
  return;
504
- if (durable?.state === 'processing') {
505
- this.messageLedger.markInterrupted(ref, this.interruptedNotice, 'html-v1');
506
- await this.deliverReadyEntry(ref);
646
+ if (durable?.state === 'processing' && queuedClaim !== ref) {
647
+ await this.interruptResponse(ref);
507
648
  return;
508
649
  }
509
650
  const selected = (0, telegram_text_entities_js_1.selectTelegramTextEntities)(message);
@@ -511,13 +652,23 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
511
652
  if (!selected.text.trim() && files.length === 0)
512
653
  return;
513
654
  const formatting = (0, telegram_text_entities_js_1.captureTelegramTextFormatting)(selected.field, selected.text, selected.entities);
514
- const inputIdentity = (0, node_crypto_1.createHash)('sha256')
515
- .update(`${selected.text}\0${files.map(({ file }) => file.file_unique_id).join(',')}`)
516
- .digest('hex');
517
- const ledgerEntry = this.messageLedger.claim(ref, {
518
- deliveryTarget: `telegram:${chatId}`,
519
- payloadIdentity: inputIdentity,
520
- }).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
+ }
521
672
  const presenter = this.createResponsePresenter(ref, Number(message.chat.id));
522
673
  this.activePresenters.set(ref, presenter);
523
674
  try {
@@ -526,8 +677,9 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
526
677
  const attachments = await (0, telegram_attachments_js_1.downloadTelegramFiles)(files, {
527
678
  api: this.bot.api,
528
679
  token: this.token,
529
- downloadsDir: privateChat
530
- ? this.principalPaths(principal.principalId).downloadsDir
680
+ downloadsDir: privateChat || memberRoom
681
+ ? this.principalPaths(principal.principalId, memberRoom ? chatId : undefined)
682
+ .downloadsDir
531
683
  : this.downloadsDir,
532
684
  messageId: message.message_id,
533
685
  });
@@ -561,6 +713,48 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
561
713
  throw error;
562
714
  }
563
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
+ }
564
758
  createResponsePresenter(sourceRef, chatId) {
565
759
  if (!this.bot)
566
760
  throw new Error('Telegram gateway not connected');
@@ -596,7 +790,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
596
790
  return;
597
791
  if (current.deliveryUncertain)
598
792
  throw new Error('Telegram response delivery is uncertain');
599
- const intake = this.responseIntake(sourceRef);
793
+ const intake = await this.responseIntake(sourceRef);
600
794
  const presenter = this.activePresenters.get(sourceRef) ?? this.createResponsePresenter(sourceRef, chatId);
601
795
  try {
602
796
  await presenter.finalize(current.response);
@@ -609,6 +803,11 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
609
803
  if (current.responseAuthor === undefined) {
610
804
  this.log(`telegram reply archive skipped key=${sourceRef} reason=missing_response_author`);
611
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
+ }
612
811
  else {
613
812
  intake.recordOwnerReply({
614
813
  messageRef: sourceRef,
@@ -626,7 +825,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
626
825
  timestamp: new Date(),
627
826
  data: { sourceMessageRef: sourceRef },
628
827
  });
629
- });
828
+ }, true);
630
829
  }
631
830
  async recoverPendingResponses() {
632
831
  for (const entry of this.messageLedger.listUndelivered()) {
@@ -636,25 +835,31 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
636
835
  continue;
637
836
  }
638
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;
639
842
  const chatId = entry.deliveryTarget?.slice('telegram:'.length);
640
843
  if (!entry.deliveryTarget?.startsWith('telegram:') || !chatId) {
641
844
  throw new Error('Telegram outbound entry has no valid destination');
642
845
  }
643
- if (!this.config.allowedChats?.includes(chatId))
644
- 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
+ }
645
851
  await this.runInChatQueue(chatId, () => this.deliverOutboundEntry(entry.key, chatId));
646
852
  continue;
647
853
  }
648
854
  if (!entry.key.startsWith('telegram:'))
649
855
  continue;
650
- const intake = this.responseIntake(entry.key);
856
+ const intake = await this.responseIntake(entry.key);
651
857
  if (entry.state === 'ready' && entry.response !== undefined) {
652
858
  await this.deliverReadyEntry(entry.key);
653
859
  continue;
654
860
  }
655
861
  if (entry.state === 'processing' && !intake.isPending?.(entry.key)) {
656
- this.messageLedger.markInterrupted(entry.key, this.interruptedNotice, 'html-v1');
657
- await this.deliverReadyEntry(entry.key);
862
+ await this.interruptResponse(entry.key);
658
863
  }
659
864
  }
660
865
  catch (error) {
@@ -672,7 +877,7 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
672
877
  await sendFormattedMessage(this.bot.api, Number(chatId), chunk);
673
878
  return;
674
879
  }
675
- const key = outboundLedgerKey(idempotencyKey);
880
+ const key = (0, telegram_message_ledger_js_1.outboundLedgerKey)(idempotencyKey);
676
881
  const binding = {
677
882
  deliveryTarget: `telegram:${chatId}`,
678
883
  payloadIdentity: (0, node_crypto_1.createHash)('sha256').update(text).digest('hex'),
@@ -682,6 +887,9 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
682
887
  const existing = this.messageLedger.claim(key, binding).entry;
683
888
  if (existing.state === 'delivered')
684
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');
685
893
  if (existing.state !== 'ready')
686
894
  this.messageLedger.markReady(key, text, 'html-v1');
687
895
  await this.deliverOutboundEntry(key, chatId);
@@ -712,7 +920,11 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
712
920
  }
713
921
  catch (error) {
714
922
  if ((0, telegram_errors_js_1.isDefinitiveTelegramRejection)(error)) {
715
- 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);
716
928
  }
717
929
  throw error;
718
930
  }
@@ -731,12 +943,14 @@ class TelegramGateway extends base_gateway_js_1.BaseGateway {
731
943
  });
732
944
  const current = (previous ?? Promise.resolve()).catch(() => { }).then(() => gate);
733
945
  this.chatTails.set(chatId, current);
946
+ const context = { chatId, active: true };
734
947
  try {
735
948
  if (previous)
736
949
  await previous.catch(() => { });
737
- return await this.activeChat.run({ chatId, active: true }, work);
950
+ return await this.activeChat.run(context, work);
738
951
  }
739
952
  finally {
953
+ context.active = false;
740
954
  release();
741
955
  if (this.chatTails.get(chatId) === current)
742
956
  this.chatTails.delete(chatId);