@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
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createMemberRecords = createMemberRecords;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  const node_fs_1 = require("node:fs");
6
- const node_path_1 = require("node:path");
7
6
  const mama_core_1 = require("@jungjaehoon/mama-core");
7
+ const telegram_message_ledger_js_1 = require("../gateways/telegram-message-ledger.js");
8
8
  const member_export_js_1 = require("./member-export.js");
9
9
  const denied = () => Object.assign(new Error('Personal records require an active member own message turn and a later confirmation'), { name: 'denied' });
10
10
  function receiptText(core, product, failure, step, omittedFiles, uncertain) {
@@ -23,7 +23,7 @@ function receiptText(core, product, failure, step, omittedFiles, uncertain) {
23
23
  ];
24
24
  if (failure)
25
25
  lines.push(uncertain
26
- ? 'The export delivery is uncertain. Nothing was erased. Check whether the file arrived before asking again.'
26
+ ? 'The export delivery is uncertain. Nothing was erased. No file was resent. Ask again for a new preview and confirm it in a later member message.'
27
27
  : ['retire', 'export', 'export_delivery', 'file_check'].includes(step)
28
28
  ? 'Nothing was erased. Ask again for a new preview and pass its confirmationToken in a later member message.'
29
29
  : 'Earlier steps are done. Make a new erasure request and pass its preview confirmationToken in a later member message to finish the rest.');
@@ -59,6 +59,9 @@ function createMemberRecords(options) {
59
59
  throw new Error('Member records require exactly one registered Telegram DM');
60
60
  return targets[0].id;
61
61
  };
62
+ const deferredInputs = (id) => options.adapter
63
+ .prepare("SELECT id, stimulus_id FROM mailbox_inputs WHERE principal_id=? AND kind='owner_message' AND status IN ('pending','claimed') AND json_extract(payload_json,'$.deferredMemberMessage')=1")
64
+ .all(id);
62
65
  const productExport = (id) => {
63
66
  const p = ports();
64
67
  const ledger = p.ledger();
@@ -89,10 +92,16 @@ function createMemberRecords(options) {
89
92
  }
90
93
  p.recordExchange(job.principalId, job.kind, `host:${job.commandId}`, text, verified);
91
94
  };
92
- // Members whose erasure is scheduled or running. Only these pause claims for everyone: the
93
- // member must stay unserved with its queue untouched until the export is delivered. An export
94
- // deletes nothing, so it holds the chain only while its archive is built.
95
+ // The confirming member alone is excluded from all claims, including host inputs.
96
+ // Its durable erasure carrier is reconciled here, outside the mailbox claim loop.
95
97
  const erasing = new Set();
98
+ const release = (id) => {
99
+ blocked.delete(id);
100
+ erasing.delete(id);
101
+ };
102
+ // The checkpoint state each running erasure holds; a write deferred into it is checkpointed
103
+ // with the state the erasure goes on to save.
104
+ const runningStates = new Map();
96
105
  // Members with an export still uploading: their turns run, but no second export or erasure
97
106
  // starts until this one is delivered, so archives and uploads never pile up.
98
107
  const exporting = new Set();
@@ -102,8 +111,8 @@ function createMemberRecords(options) {
102
111
  .catch((error) => console.error('[Member records] scheduled work failed', error))
103
112
  .finally(() => jobs.delete(work));
104
113
  };
105
- const enqueue = (work) => {
106
- track(ports().turnChain.next(async () => {
114
+ const enqueue = (id, work) => {
115
+ const runSlot = async () => {
107
116
  let leaveChain;
108
117
  const left = new Promise((resolve) => (leaveChain = resolve));
109
118
  const run = work(leaveChain);
@@ -115,7 +124,8 @@ function createMemberRecords(options) {
115
124
  if (!ports().isStopping())
116
125
  ports().resumeQueued();
117
126
  }
118
- }));
127
+ };
128
+ track(ports().turnChain(id).next(runSlot));
119
129
  };
120
130
  const take = (row) => {
121
131
  const job = scheduled.get(row.id);
@@ -128,11 +138,24 @@ function createMemberRecords(options) {
128
138
  const job = take(row);
129
139
  if (!job)
130
140
  return;
131
- blocked.delete(job.principalId);
132
- erasing.delete(job.principalId);
141
+ if (job.kind === 'erase' && ports().isStopping()) {
142
+ // Native stop can interrupt the confirming turn after the host accepted its command.
143
+ // The consent is durable; stopping the process is not cancellation of that consent.
144
+ job.release();
145
+ return;
146
+ }
147
+ if (job.kind === 'erase') {
148
+ cancelErasureConfirmation(job, outcome, reason);
149
+ launchErasure(job);
150
+ job.release();
151
+ return;
152
+ }
153
+ if (job.hostInputId !== undefined)
154
+ ports().mailbox().ack(job.hostInputId);
155
+ release(job.principalId);
133
156
  if (!ports().isStopping()) {
134
- const label = job.kind === 'erase' ? 'Erasure' : 'Export';
135
- enqueue(() => sendReceipt(job, `${label} did not run because the ${job.kind === 'erase' ? 'confirming' : 'requesting'} turn did not complete (${outcome}: ${reason}).\nYour personal records and files were kept. Make a new request in a later member message.\nYour enrollment and grants are kept.`));
157
+ const label = 'Export';
158
+ enqueue(job.principalId, () => sendReceipt(job, `${label} did not run because the ${job.kind === 'erase' ? 'confirming' : 'requesting'} turn did not complete (${outcome}: ${reason}).\nYour personal records and files were kept. Make a new request in a later member message.\nYour enrollment and grants are kept.`));
136
159
  }
137
160
  job.release();
138
161
  };
@@ -140,7 +163,7 @@ function createMemberRecords(options) {
140
163
  const p = ports(), id = job.principalId;
141
164
  let left = false;
142
165
  // An export's archive is a consistent snapshot once built: the member may run turns again
143
- // and the upload continues outside the shared chain.
166
+ // and the upload continues outside that member's chain.
144
167
  const leave = () => {
145
168
  if (left || job.kind !== 'export')
146
169
  return;
@@ -149,14 +172,10 @@ function createMemberRecords(options) {
149
172
  blocked.delete(id);
150
173
  leaveChain();
151
174
  };
152
- const product = { cancelledMessages: 0 };
153
- let core;
154
175
  let archive;
155
- let failedStep = job.kind === 'erase' ? 'retire' : 'export', failure, uncertain = false;
176
+ let failedStep = 'export', failure, uncertain = false;
156
177
  let delivered = false;
157
178
  try {
158
- if (job.kind === 'erase')
159
- await p.retire(id);
160
179
  failedStep = 'export';
161
180
  archive = await (0, member_export_js_1.buildMemberExport)(p.root, id, (0, mama_core_1.exportPrincipalRecords)(options.adapter, id), productExport(id));
162
181
  leave();
@@ -178,69 +197,14 @@ function createMemberRecords(options) {
178
197
  finally {
179
198
  (0, node_fs_1.rmSync)(archive.path, { force: true });
180
199
  }
181
- if (job.kind === 'erase') {
182
- // Retirement removed the writers and intake before the snapshot. Check and delete
183
- // synchronously after delivery, with no await between them that could admit a write.
184
- failedStep = 'file_check';
185
- const inventory = (0, member_export_js_1.memberFileInventory)(p.root, id);
186
- if ((0, member_export_js_1.memberInventorySnapshot)(inventory) !== archive.fileSnapshot)
187
- throw new Error('Your files changed while the export was being sent');
188
- failedStep = 'files';
189
- product.files = 0;
190
- product.fileBytes = 0;
191
- product.fileTrees = 0;
192
- for (const tree of inventory.trees) {
193
- const remaining = inventory.allFiles.filter((file) => (0, node_path_1.relative)(tree, file.path) !== '..' && !(0, node_path_1.relative)(tree, file.path).startsWith('../'));
194
- (0, node_fs_1.rmSync)(tree, { recursive: true, force: true });
195
- product.files += remaining.length;
196
- product.fileBytes += remaining.reduce((n, file) => n + file.size, 0);
197
- product.fileTrees++;
198
- }
199
- failedStep = 'queued_inputs';
200
- // Delivered in the export and never dispatched: cancelled here, they are terminal, so
201
- // the core erase below removes them with their refs.
202
- product.cancelledMessages = p.mailbox().cancelQueued(id, 'member_erase');
203
- failedStep = 'connector_event_index';
204
- product.connector_event_index = options.adapter
205
- .prepare("DELETE FROM connector_event_index WHERE source_connector='chat' AND memory_scope_kind='user' AND memory_scope_id=?")
206
- .run(id).changes;
207
- failedStep = 'core';
208
- core = (0, mama_core_1.erasePrincipalRecords)(options.adapter, {
209
- principalId: id,
210
- commandId: job.commandId,
211
- });
212
- failedStep = 'chat_raw';
213
- Object.assign(product, p.rawStore.eraseScope('chat', 'user', id));
214
- failedStep = 'message_ledger';
215
- const ledger = p.ledger();
216
- if (!ledger)
217
- throw new Error('Live message ledger is not configured');
218
- product.message_ledger = ledger.eraseTelegramDm(dm(p.access(id)), [
219
- `file:${job.commandId}:export`,
220
- ]);
221
- }
222
200
  }
223
201
  catch (error) {
224
202
  failure = error;
225
203
  }
226
204
  try {
227
- try {
228
- p.serve(id);
229
- }
230
- catch (error) {
231
- // Keep the step that stopped the job: it decides what was erased and what remains.
232
- if (failure === undefined) {
233
- failedStep = 'serve';
234
- failure = error;
235
- }
236
- else
237
- failure = new Error(`${failure instanceof Error ? failure.message : String(failure)}; serving the member again also failed: ${error instanceof Error ? error.message : String(error)}`);
238
- }
239
205
  const omittedFiles = archive?.omittedFiles ?? [];
240
206
  let text;
241
- if (job.kind === 'erase')
242
- text = receiptText(core, product, failure, failedStep, omittedFiles, uncertain);
243
- else {
207
+ {
244
208
  text = uncertain
245
209
  ? `Export delivery is uncertain at ${failedStep}: ${failure instanceof Error ? failure.message : String(failure)}.`
246
210
  : failure
@@ -250,7 +214,7 @@ function createMemberRecords(options) {
250
214
  : 'Export sent: your personal records and files were sent to your registered Telegram DM.';
251
215
  if (uncertain)
252
216
  text +=
253
- '\nThe export delivery is uncertain. Check whether the file arrived before asking again.';
217
+ '\nThe export delivery is uncertain. No file was resent. Check your DM for the export before requesting another export.';
254
218
  text += `\nFiles: ${archive?.fileCount ?? 0} files, ${archive?.fileBytes ?? 0} bytes.\nYour enrollment and grants are kept.`;
255
219
  for (const name of omittedFiles)
256
220
  text += `\nLeft out of the export (a link is outside your files or in a private host directory): ${name}.`;
@@ -263,19 +227,363 @@ function createMemberRecords(options) {
263
227
  exporting.delete(id);
264
228
  }
265
229
  else {
266
- blocked.delete(id);
267
- erasing.delete(id);
230
+ release(id);
268
231
  }
269
232
  }
270
233
  };
234
+ const runningErasures = new Set();
235
+ let recoveryTimer;
236
+ const checkpoint = (job, state) => {
237
+ // Consumer progress is opaque to core; the host stimulus identity stays immutable.
238
+ ports()
239
+ .mailbox()
240
+ .checkpointHostJob(job.hostInputId, JSON.parse(JSON.stringify(state)));
241
+ };
242
+ const cancelErasureConfirmation = (job, outcome, reason) => {
243
+ const state = ports().mailbox().hostJobCheckpoint(job.hostInputId);
244
+ state.phase = 'receipt';
245
+ state.text = `Erasure did not run because the confirming turn did not complete (${outcome}: ${reason}).\nYour personal records and files were kept. Make a new request in a later member message.\nYour enrollment and grants are kept.`;
246
+ checkpoint(job, state);
247
+ };
248
+ const runErasure = async (job) => {
249
+ const p = ports(), id = job.principalId;
250
+ const state = p.mailbox().hostJobCheckpoint(job.hostInputId);
251
+ runningStates.set(id, { job, state });
252
+ if (state.phase === 'done') {
253
+ p.serve(id);
254
+ p.mailbox().ack(job.hostInputId);
255
+ release(id);
256
+ return;
257
+ }
258
+ let step = 'retire';
259
+ let uncertainExport = false;
260
+ try {
261
+ if (state.phase === 'confirmed') {
262
+ if (p.isStopping())
263
+ return;
264
+ await p.retire(id);
265
+ if (p.isStopping())
266
+ return;
267
+ step = 'export';
268
+ const product = productExport(id);
269
+ const ledger = p.ledger();
270
+ state.exported = {
271
+ ledgerKeys: ledger.listForTelegramDm(dm(p.access(id))).map((entry) => entry.key),
272
+ };
273
+ state.archive = await (0, member_export_js_1.buildMemberExport)(p.root, id, (0, mama_core_1.exportPrincipalRecords)(options.adapter, id), product);
274
+ state.phase = 'exporting';
275
+ checkpoint(job, state);
276
+ }
277
+ if (state.phase === 'exporting') {
278
+ if (p.isStopping())
279
+ return;
280
+ step = 'export_delivery';
281
+ // A boot recreates native directories. Retire them before checking the persisted snapshot.
282
+ await p.retire(id);
283
+ if (p.isStopping())
284
+ return;
285
+ // This principal remains reserved and excluded. Its native writers are gone and the
286
+ // archive touches only its known paths; other directory jobs need not wait for upload.
287
+ p.turnChain(id).releaseDirectoryWait();
288
+ const entry = p.ledger()?.get(`file:${job.commandId}:export`);
289
+ if (entry?.state !== 'delivered') {
290
+ // The ledger has retention. A lost/expired receipt after a persisted send boundary
291
+ // cannot turn an attempted upload back into a never-attempted one.
292
+ if (!entry && state.exportAttempted) {
293
+ uncertainExport = true;
294
+ throw new Error('The export delivery is uncertain: its receipt is missing');
295
+ }
296
+ // A persisted attempt without a definitive receipt is never permission to delete.
297
+ // In particular, a processing entry left by a crash may have reached Telegram.
298
+ if (entry && (entry.deliveryUncertain || entry.state === 'processing'))
299
+ throw new Error('The export delivery is uncertain');
300
+ if (!state.archive)
301
+ throw new Error('The export snapshot is missing');
302
+ if (entry)
303
+ throw new Error('The export delivery was not confirmed');
304
+ state.exportAttempted = true;
305
+ checkpoint(job, state);
306
+ try {
307
+ const telegram = p.telegram();
308
+ if (!telegram)
309
+ throw new Error('Member Telegram delivery is not configured');
310
+ await telegram.sendFile(state.archive.path, undefined, `${job.commandId}:export`, {
311
+ access: p.access(id),
312
+ filesRoot: state.archive.filesRoot,
313
+ });
314
+ }
315
+ catch (error) {
316
+ // A send that failed before the transport claimed its ledger entry never reached
317
+ // Telegram; clear the attempt so a resume does not report an uncertain delivery.
318
+ if (!p.ledger()?.get(`file:${job.commandId}:export`)) {
319
+ state.exportAttempted = false;
320
+ checkpoint(job, state);
321
+ }
322
+ throw error;
323
+ }
324
+ }
325
+ state.phase = 'delivered';
326
+ checkpoint(job, state);
327
+ if (p.isStopping())
328
+ return;
329
+ }
330
+ if (state.phase === 'delivered') {
331
+ if (p.isStopping())
332
+ return;
333
+ await p.retire(id);
334
+ if (p.isStopping())
335
+ return;
336
+ if (state.archive)
337
+ (0, node_fs_1.rmSync)(state.archive.path, { force: true });
338
+ step = 'file_check';
339
+ const inventory = (0, member_export_js_1.memberFileInventory)(p.root, id);
340
+ if (!state.archive || (0, member_export_js_1.memberInventorySnapshot)(inventory) !== state.archive.fileSnapshot)
341
+ throw new Error('Your files changed while the export was being sent');
342
+ state.product.files = inventory.allFiles.length;
343
+ state.product.fileBytes = inventory.allFiles.reduce((n, file) => n + file.size, 0);
344
+ state.product.fileTrees = inventory.trees.length;
345
+ state.phase = 'erasing';
346
+ checkpoint(job, state); // Deletion may now replay each remaining store, never the upload.
347
+ }
348
+ if (state.phase === 'erasing') {
349
+ // Checked before any deletion: the ledger erase below keeps entries outside this snapshot.
350
+ if (!state.exported)
351
+ throw new Error('The export ledger snapshot is missing');
352
+ step = 'files';
353
+ // No await between the delivered file check and the deletion on the first run.
354
+ for (const tree of (0, member_export_js_1.memberFileInventory)(p.root, id).trees)
355
+ (0, node_fs_1.rmSync)(tree, { recursive: true, force: true });
356
+ const product = { ...state.product };
357
+ let core = state.core;
358
+ options.adapter.transaction(() => {
359
+ product.cancelledMessages += p.mailbox().cancelQueued(id, 'member_erase', deferredInputs(id).map((row) => row.id));
360
+ step = 'connector_event_index';
361
+ // Core erases the person's scope, so its chat rows go with it; personal host receipts
362
+ // written during the erasure are deferred until it ends (deferWhileErasing).
363
+ product.connector_event_index =
364
+ (product.connector_event_index ?? 0) +
365
+ options.adapter
366
+ .prepare("DELETE FROM connector_event_index WHERE source_connector='chat' AND memory_scope_kind='user' AND memory_scope_id=?")
367
+ .run(id).changes;
368
+ step = 'core';
369
+ core = (0, mama_core_1.erasePrincipalRecords)(options.adapter, {
370
+ principalId: id,
371
+ commandId: job.commandId,
372
+ });
373
+ checkpoint(job, { ...state, product, core });
374
+ });
375
+ // A rolled-back transaction must not report its attempted deletes as completed.
376
+ state.product = product;
377
+ state.core = core;
378
+ step = 'chat_raw';
379
+ // Save intended totals before crossing the independent raw database/file-ledger boundary.
380
+ const rawCounts = p.rawStore.exportScope('chat', 'user', id);
381
+ for (const [name, rows] of Object.entries(rawCounts))
382
+ state.product[name] ??= rows.length;
383
+ const ledger = p.ledger();
384
+ if (!ledger)
385
+ throw new Error('Live message ledger is not configured');
386
+ const exportedKeys = new Set(state.exported.ledgerKeys);
387
+ // Entries written for this DM after the snapshot (another principal's delivery, a
388
+ // management receipt) were not exported, so they stay.
389
+ const later = ledger
390
+ .listForTelegramDm(dm(p.access(id)))
391
+ .filter((entry) => !exportedKeys.has(entry.key))
392
+ .map((entry) => entry.key);
393
+ state.product.message_ledger ??= ledger
394
+ .listForTelegramDm(dm(p.access(id)))
395
+ .filter((entry) => exportedKeys.has(entry.key) &&
396
+ entry.key !== `file:${job.commandId}:export` &&
397
+ !deferredInputs(id).some((row) => row.stimulus_id === entry.key)).length;
398
+ checkpoint(job, state);
399
+ p.rawStore.eraseScope('chat', 'user', id);
400
+ step = 'message_ledger';
401
+ ledger.eraseTelegramDm(dm(p.access(id)), [
402
+ `file:${job.commandId}:export`,
403
+ ...deferredInputs(id).map((row) => row.stimulus_id),
404
+ ...later,
405
+ ]);
406
+ state.text = receiptText(state.core, state.product, undefined, '', state.archive?.omittedFiles ?? [], false);
407
+ state.phase = 'receipt';
408
+ delete state.archive;
409
+ checkpoint(job, state);
410
+ }
411
+ }
412
+ catch (error) {
413
+ // Retirement races shutdown before any deletion. Keep the confirmed job resumable.
414
+ if (p.isStopping() && ['confirmed', 'exporting', 'delivered'].includes(state.phase))
415
+ return;
416
+ const entry = p.ledger()?.get(`file:${job.commandId}:export`);
417
+ const uncertain = uncertainExport ||
418
+ (state.phase === 'exporting' &&
419
+ (entry?.deliveryUncertain === true || entry?.state === 'processing'));
420
+ state.text = receiptText(state.core, state.product, error, step, state.archive?.omittedFiles ?? [], uncertain);
421
+ state.phase = 'receipt';
422
+ checkpoint(job, state);
423
+ }
424
+ if (state.phase === 'receipt') {
425
+ if (state.archive)
426
+ (0, node_fs_1.rmSync)(state.archive.path, { force: true });
427
+ if (p.isStopping())
428
+ return;
429
+ try {
430
+ p.serve(id);
431
+ }
432
+ catch (error) {
433
+ if (!state.receiptAttempted && !state.receiptDelivered && !state.serveFailed) {
434
+ state.text = `${state.text}\nserving the member again also failed: ${error instanceof Error ? error.message : String(error)}.`;
435
+ state.serveFailed = true;
436
+ checkpoint(job, state);
437
+ }
438
+ else {
439
+ // The receipt names the first failure once (the recovery tick retries every few
440
+ // seconds), and a text that crossed the send boundary never changes.
441
+ console.error('[Member records] service restoration failed again', error);
442
+ }
443
+ // Keep the receipt carrier and principal exclusion until service restoration succeeds.
444
+ // The recovery tick retries this phase; deferred messages must not run unserved.
445
+ throw error;
446
+ }
447
+ const telegram = p.telegram();
448
+ p.turnChain(id).releaseDirectoryWait();
449
+ if (!telegram)
450
+ throw new Error('Member Telegram delivery is not configured');
451
+ const operation = `${job.commandId}:receipt`;
452
+ if (!state.receiptDelivered) {
453
+ state.receiptDelivered = telegram.memberTextDelivered(p.access(id), operation);
454
+ if (!state.receiptDelivered) {
455
+ const entry = p.ledger()?.get((0, telegram_message_ledger_js_1.outboundLedgerKey)(operation));
456
+ // The transport claims its ledger entry before sending, so no entry means nothing was
457
+ // sent: send again (a duplicate receipt is harmless; a lost one is not). Only an
458
+ // uncertain claimed send is final.
459
+ if (!entry?.deliveryUncertain) {
460
+ state.receiptAttempted = true;
461
+ checkpoint(job, state);
462
+ try {
463
+ state.receiptDelivered = await telegram.sendMemberText(p.access(id), state.text, operation);
464
+ if (!state.receiptDelivered)
465
+ throw new Error('Erasure receipt delivery was not confirmed');
466
+ }
467
+ catch (error) {
468
+ const attempted = p.ledger()?.get((0, telegram_message_ledger_js_1.outboundLedgerKey)(operation));
469
+ // A failed pre-claim call is retryable. An uncertain send is final for this
470
+ // data job: record it unverified without resending or blocking later turns.
471
+ if (!attempted) {
472
+ state.receiptAttempted = false;
473
+ checkpoint(job, state);
474
+ }
475
+ if (!attempted?.deliveryUncertain)
476
+ throw error;
477
+ }
478
+ }
479
+ }
480
+ checkpoint(job, state);
481
+ }
482
+ p.recordExchange(id, 'erase', `host:${job.commandId}`, state.text, state.receiptDelivered === true);
483
+ // Outside the transaction: the raw store commits apart from it, and a repeated write of a
484
+ // saved exchange returns the stored one, so a retry after a failure here finishes.
485
+ for (const exchange of state.afterErasure ?? [])
486
+ p.writeHostExchange(id, exchange);
487
+ options.adapter.transaction(() => {
488
+ state.phase = 'done';
489
+ delete state.archive;
490
+ delete state.exported;
491
+ delete state.afterErasure;
492
+ checkpoint(job, state);
493
+ p.mailbox().ack(job.hostInputId);
494
+ });
495
+ release(id);
496
+ }
497
+ };
498
+ const launchErasure = (job) => {
499
+ if (runningErasures.has(job.hostInputId))
500
+ return;
501
+ runningErasures.add(job.hostInputId);
502
+ enqueue(job.principalId, async () => {
503
+ try {
504
+ await runErasure(job);
505
+ }
506
+ finally {
507
+ runningErasures.delete(job.hostInputId);
508
+ runningStates.delete(job.principalId);
509
+ }
510
+ });
511
+ };
512
+ const durableRows = () => options.ports
513
+ ? options.adapter
514
+ .prepare("SELECT id, stimulus_id, principal_id, payload_json FROM mailbox_inputs WHERE kind='host_job' AND channel_key='member-erasure' AND status IN ('pending','claimed') ORDER BY id")
515
+ .all()
516
+ : [];
517
+ // Restore exclusion before any boot-time claim. A live confirmation still waits for settlement;
518
+ // after a crash its process no longer owns files, so recovery can take the same member slot.
519
+ for (const row of durableRows()) {
520
+ blocked.add(row.principal_id);
521
+ erasing.add(row.principal_id);
522
+ }
523
+ const recover = () => {
524
+ // An assembly without member_root has no personal-record host runner or delivery ports.
525
+ if (!options.ports)
526
+ return;
527
+ if (ports().isStopping() || !ports().isReady())
528
+ return;
529
+ for (const row of durableRows()) {
530
+ const payload = JSON.parse(row.payload_json);
531
+ if (scheduled.has(payload.inputId))
532
+ continue;
533
+ // Core reconciliation owns uncertain native intake, even after its process is gone.
534
+ const mailbox = ports().mailbox();
535
+ const status = mailbox.inputStatus(payload.inputId);
536
+ const confirmation = mailbox.nativeInputs.get(payload.inputId);
537
+ const job = {
538
+ principalId: row.principal_id,
539
+ inputId: payload.inputId,
540
+ commandId: row.stimulus_id,
541
+ kind: 'erase',
542
+ hostInputId: row.id,
543
+ release: () => { },
544
+ };
545
+ const state = mailbox.hostJobCheckpoint(row.id);
546
+ // A confirmed command is still waiting for native settlement. A crash before the
547
+ // cancellation callback cannot turn an incomplete confirming turn into permission to erase.
548
+ if (state.phase === 'confirmed' && (status !== 'acked' || confirmation?.state !== 'settled'))
549
+ cancelErasureConfirmation(job, status === 'dead' ? 'dead' : (confirmation?.state ?? 'unsettled'), confirmation?.error ??
550
+ `confirmation input status=${status ?? 'missing'}, native state=${confirmation?.state ?? 'missing'}`);
551
+ if (status === 'acked' || status === 'dead')
552
+ mailbox.quarantine(payload.inputId, 'member_erasure_confirmation_recovered');
553
+ launchErasure(job);
554
+ }
555
+ };
271
556
  const schedule = (kind, row) => {
272
557
  const commandId = `member-${kind}:${row.principalId}:${row.id}`;
558
+ let hostInputId;
559
+ if (kind === 'erase')
560
+ options.adapter.transaction(() => {
561
+ hostInputId =
562
+ ports()
563
+ .mailbox()
564
+ .enqueue({
565
+ id: commandId,
566
+ kind: 'host_job',
567
+ principalId: row.principalId,
568
+ channelKey: 'member-erasure',
569
+ payload: { inputId: row.id },
570
+ occurredAt: Date.now(),
571
+ }) ?? ports().mailbox().readInput(commandId, row.principalId).id;
572
+ if (!ports().mailbox().hostJobCheckpoint(hostInputId))
573
+ ports()
574
+ .mailbox()
575
+ .checkpointHostJob(hostInputId, {
576
+ phase: 'confirmed',
577
+ product: { cancelledMessages: 0 },
578
+ });
579
+ });
273
580
  scheduled.set(row.id, {
274
581
  principalId: row.principalId,
275
582
  inputId: row.id,
276
583
  commandId,
277
584
  kind,
278
- release: ports().turnChain.hold(),
585
+ ...(hostInputId === undefined ? {} : { hostInputId }),
586
+ release: ports().turnChain(row.principalId).hold(),
279
587
  });
280
588
  blocked.add(row.principalId);
281
589
  if (kind === 'erase')
@@ -356,17 +664,67 @@ function createMemberRecords(options) {
356
664
  return {
357
665
  registrations,
358
666
  isBusy: () => erasing.size > 0,
667
+ excludePrincipals: () => [...erasing],
668
+ /**
669
+ * Keep a personal write for after this member's erasure once its export snapshot is taken;
670
+ * false means the caller writes it now (no erasure, or one whose export will still hold it).
671
+ */
672
+ deferWhileErasing: (id, exchange) => {
673
+ const running = runningStates.get(id);
674
+ if (running) {
675
+ if (!running.state.exported)
676
+ return false;
677
+ (running.state.afterErasure ??= []).push(exchange);
678
+ checkpoint(running.job, running.state);
679
+ return true;
680
+ }
681
+ // Not running here (a stop interrupted it, or recovery has not resumed it yet): its
682
+ // durable checkpoint carries the write to whichever run finishes the erasure.
683
+ const row = durableRows().find((candidate) => candidate.principal_id === id);
684
+ if (!row)
685
+ return false;
686
+ const mailbox = ports().mailbox();
687
+ const state = mailbox.hostJobCheckpoint(row.id);
688
+ if (!state.exported)
689
+ return false;
690
+ (state.afterErasure ??= []).push(exchange);
691
+ mailbox.checkpointHostJob(row.id, JSON.parse(JSON.stringify(state)));
692
+ return true;
693
+ },
694
+ recover,
695
+ startRecovery: () => {
696
+ if (!options.ports)
697
+ return;
698
+ recover();
699
+ recoveryTimer ??= setInterval(recover, 5_000);
700
+ recoveryTimer.unref();
701
+ },
702
+ stopRecovery: () => {
703
+ if (recoveryTimer)
704
+ clearInterval(recoveryTimer);
705
+ recoveryTimer = undefined;
706
+ },
359
707
  isBlocked: (id) => blocked.has(id),
360
708
  onSettled: (row) => {
361
709
  const job = take(row);
362
710
  if (!job)
363
711
  return;
364
712
  if (ports().isStopping()) {
365
- blocked.delete(job.principalId);
366
- erasing.delete(job.principalId);
713
+ if (job.kind !== 'erase') {
714
+ blocked.delete(job.principalId);
715
+ // Exports have no durable carrier; the transports stay open through the drain, so say
716
+ // so instead of dropping the request silently.
717
+ enqueue(job.principalId, () => sendReceipt(job, 'Export did not run because MAMA was restarting. Ask again in a later message.'));
718
+ }
719
+ }
720
+ else {
721
+ // Export writes workspace/files; erasure recreates this member's runtime/temp paths.
722
+ // Other members already deny those paths, so both use only this member's chain.
723
+ if (job.kind === 'erase')
724
+ launchErasure(job);
725
+ else
726
+ enqueue(job.principalId, (leaveChain) => runJob(job, leaveChain));
367
727
  }
368
- else
369
- enqueue((leaveChain) => runJob(job, leaveChain));
370
728
  job.release();
371
729
  },
372
730
  onDead: (row, reason) => cancelConfirmation(row, 'dead', reason),