@jungjaehoon/mama-os 0.75.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 (237) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +1 -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/member-enrollment.d.ts +7 -3
  12. package/dist/api/member-enrollment.d.ts.map +1 -1
  13. package/dist/api/member-enrollment.js +64 -16
  14. package/dist/api/member-enrollment.js.map +1 -1
  15. package/dist/api/member-export.d.ts.map +1 -1
  16. package/dist/api/member-export.js +14 -6
  17. package/dist/api/member-export.js.map +1 -1
  18. package/dist/api/member-lifecycle.d.ts +23 -4
  19. package/dist/api/member-lifecycle.d.ts.map +1 -1
  20. package/dist/api/member-lifecycle.js +88 -21
  21. package/dist/api/member-lifecycle.js.map +1 -1
  22. package/dist/api/member-notice-actions.d.ts +18 -0
  23. package/dist/api/member-notice-actions.d.ts.map +1 -0
  24. package/dist/api/member-notice-actions.js +184 -0
  25. package/dist/api/member-notice-actions.js.map +1 -0
  26. package/dist/api/member-records.d.ts +17 -2
  27. package/dist/api/member-records.d.ts.map +1 -1
  28. package/dist/api/member-records.js +440 -82
  29. package/dist/api/member-records.js.map +1 -1
  30. package/dist/api/member-share-actions.d.ts +3 -3
  31. package/dist/api/member-share-actions.d.ts.map +1 -1
  32. package/dist/api/member-share-actions.js +47 -20
  33. package/dist/api/member-share-actions.js.map +1 -1
  34. package/dist/api/owner-policy-actions.d.ts +1 -0
  35. package/dist/api/owner-policy-actions.d.ts.map +1 -1
  36. package/dist/api/owner-policy-actions.js +8 -4
  37. package/dist/api/owner-policy-actions.js.map +1 -1
  38. package/dist/api/owner-timezone-actions.d.ts +3 -2
  39. package/dist/api/owner-timezone-actions.d.ts.map +1 -1
  40. package/dist/api/owner-timezone-actions.js +4 -6
  41. package/dist/api/owner-timezone-actions.js.map +1 -1
  42. package/dist/api/personal-job-actions.d.ts +6 -0
  43. package/dist/api/personal-job-actions.d.ts.map +1 -0
  44. package/dist/api/personal-job-actions.js +72 -0
  45. package/dist/api/personal-job-actions.js.map +1 -0
  46. package/dist/api/security-events.d.ts +6 -1
  47. package/dist/api/security-events.d.ts.map +1 -1
  48. package/dist/api/security-events.js +34 -15
  49. package/dist/api/security-events.js.map +1 -1
  50. package/dist/api/skill-actions.d.ts +5 -0
  51. package/dist/api/skill-actions.d.ts.map +1 -0
  52. package/dist/api/skill-actions.js +66 -0
  53. package/dist/api/skill-actions.js.map +1 -0
  54. package/dist/api/source-actions.d.ts.map +1 -1
  55. package/dist/api/source-actions.js +4 -2
  56. package/dist/api/source-actions.js.map +1 -1
  57. package/dist/api/stored-source-reader.d.ts.map +1 -1
  58. package/dist/api/stored-source-reader.js +9 -2
  59. package/dist/api/stored-source-reader.js.map +1 -1
  60. package/dist/api/trello-actions.d.ts.map +1 -1
  61. package/dist/api/trello-actions.js +8 -2
  62. package/dist/api/trello-actions.js.map +1 -1
  63. package/dist/api/work-actions.d.ts +12 -2
  64. package/dist/api/work-actions.d.ts.map +1 -1
  65. package/dist/api/work-actions.js +57 -10
  66. package/dist/api/work-actions.js.map +1 -1
  67. package/dist/cli/commands/daemon.d.ts +12 -2
  68. package/dist/cli/commands/daemon.d.ts.map +1 -1
  69. package/dist/cli/commands/daemon.js +76 -12
  70. package/dist/cli/commands/daemon.js.map +1 -1
  71. package/dist/cli/commands/init.d.ts +4 -0
  72. package/dist/cli/commands/init.d.ts.map +1 -1
  73. package/dist/cli/commands/init.js +23 -0
  74. package/dist/cli/commands/init.js.map +1 -1
  75. package/dist/cli/commands/tools.d.ts +12 -0
  76. package/dist/cli/commands/tools.d.ts.map +1 -0
  77. package/dist/cli/commands/tools.js +109 -0
  78. package/dist/cli/commands/tools.js.map +1 -0
  79. package/dist/cli/index.js +6 -1
  80. package/dist/cli/index.js.map +1 -1
  81. package/dist/connectors/framework/polling-scheduler.d.ts +6 -1
  82. package/dist/connectors/framework/polling-scheduler.d.ts.map +1 -1
  83. package/dist/connectors/framework/polling-scheduler.js +25 -3
  84. package/dist/connectors/framework/polling-scheduler.js.map +1 -1
  85. package/dist/connectors/framework/stored-index-read.d.ts +2 -2
  86. package/dist/connectors/framework/stored-index-read.d.ts.map +1 -1
  87. package/dist/connectors/framework/stored-index-read.js +10 -3
  88. package/dist/connectors/framework/stored-index-read.js.map +1 -1
  89. package/dist/gateways/base-gateway.d.ts +7 -0
  90. package/dist/gateways/base-gateway.d.ts.map +1 -1
  91. package/dist/gateways/base-gateway.js +24 -0
  92. package/dist/gateways/base-gateway.js.map +1 -1
  93. package/dist/gateways/discord.d.ts +4 -0
  94. package/dist/gateways/discord.d.ts.map +1 -1
  95. package/dist/gateways/discord.js +24 -6
  96. package/dist/gateways/discord.js.map +1 -1
  97. package/dist/gateways/slack.d.ts +3 -0
  98. package/dist/gateways/slack.d.ts.map +1 -1
  99. package/dist/gateways/slack.js +15 -2
  100. package/dist/gateways/slack.js.map +1 -1
  101. package/dist/gateways/telegram-errors.d.ts +3 -0
  102. package/dist/gateways/telegram-errors.d.ts.map +1 -1
  103. package/dist/gateways/telegram-errors.js +5 -0
  104. package/dist/gateways/telegram-errors.js.map +1 -1
  105. package/dist/gateways/telegram-message-ledger.d.ts +7 -0
  106. package/dist/gateways/telegram-message-ledger.d.ts.map +1 -1
  107. package/dist/gateways/telegram-message-ledger.js +27 -2
  108. package/dist/gateways/telegram-message-ledger.js.map +1 -1
  109. package/dist/gateways/telegram.d.ts +24 -6
  110. package/dist/gateways/telegram.d.ts.map +1 -1
  111. package/dist/gateways/telegram.js +258 -44
  112. package/dist/gateways/telegram.js.map +1 -1
  113. package/dist/gateways/turn-contract.d.ts +6 -0
  114. package/dist/gateways/turn-contract.d.ts.map +1 -1
  115. package/dist/runtime/action-surface.d.ts +28 -3
  116. package/dist/runtime/action-surface.d.ts.map +1 -1
  117. package/dist/runtime/action-surface.js +310 -34
  118. package/dist/runtime/action-surface.js.map +1 -1
  119. package/dist/runtime/backend-security.d.ts +4 -0
  120. package/dist/runtime/backend-security.d.ts.map +1 -1
  121. package/dist/runtime/backend-security.js +8 -1
  122. package/dist/runtime/backend-security.js.map +1 -1
  123. package/dist/runtime/config.d.ts +1 -0
  124. package/dist/runtime/config.d.ts.map +1 -1
  125. package/dist/runtime/config.js +11 -0
  126. package/dist/runtime/config.js.map +1 -1
  127. package/dist/runtime/connectors.js +1 -1
  128. package/dist/runtime/connectors.js.map +1 -1
  129. package/dist/runtime/egress-proxy.d.ts +3 -3
  130. package/dist/runtime/egress-proxy.d.ts.map +1 -1
  131. package/dist/runtime/egress-proxy.js +16 -3
  132. package/dist/runtime/egress-proxy.js.map +1 -1
  133. package/dist/runtime/member-paths.d.ts +9 -1
  134. package/dist/runtime/member-paths.d.ts.map +1 -1
  135. package/dist/runtime/member-paths.js +67 -5
  136. package/dist/runtime/member-paths.js.map +1 -1
  137. package/dist/runtime/member-session.d.ts +21 -21
  138. package/dist/runtime/member-session.d.ts.map +1 -1
  139. package/dist/runtime/member-session.js +289 -34
  140. package/dist/runtime/member-session.js.map +1 -1
  141. package/dist/runtime/member-system-prompt.d.ts +1 -0
  142. package/dist/runtime/member-system-prompt.d.ts.map +1 -1
  143. package/dist/runtime/member-system-prompt.js +22 -4
  144. package/dist/runtime/member-system-prompt.js.map +1 -1
  145. package/dist/runtime/native-session.d.ts +6 -0
  146. package/dist/runtime/native-session.d.ts.map +1 -1
  147. package/dist/runtime/native-session.js +80 -13
  148. package/dist/runtime/native-session.js.map +1 -1
  149. package/dist/runtime/outbound-attempts.d.ts +5 -2
  150. package/dist/runtime/outbound-attempts.d.ts.map +1 -1
  151. package/dist/runtime/outbound-attempts.js +3 -2
  152. package/dist/runtime/outbound-attempts.js.map +1 -1
  153. package/dist/runtime/owner-authority.d.ts +14 -14
  154. package/dist/runtime/owner-authority.d.ts.map +1 -1
  155. package/dist/runtime/owner-authority.js +159 -7
  156. package/dist/runtime/owner-authority.js.map +1 -1
  157. package/dist/runtime/owner-runtime.d.ts +16 -5
  158. package/dist/runtime/owner-runtime.d.ts.map +1 -1
  159. package/dist/runtime/owner-runtime.js +426 -43
  160. package/dist/runtime/owner-runtime.js.map +1 -1
  161. package/dist/runtime/owner-system-prompt.d.ts +3 -1
  162. package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
  163. package/dist/runtime/owner-system-prompt.js +15 -5
  164. package/dist/runtime/owner-system-prompt.js.map +1 -1
  165. package/dist/runtime/personal-job-cron.d.ts +6 -0
  166. package/dist/runtime/personal-job-cron.d.ts.map +1 -0
  167. package/dist/runtime/personal-job-cron.js +154 -0
  168. package/dist/runtime/personal-job-cron.js.map +1 -0
  169. package/dist/runtime/personal-jobs.d.ts +107 -0
  170. package/dist/runtime/personal-jobs.d.ts.map +1 -0
  171. package/dist/runtime/personal-jobs.js +353 -0
  172. package/dist/runtime/personal-jobs.js.map +1 -0
  173. package/dist/runtime/personal-notices.d.ts +38 -0
  174. package/dist/runtime/personal-notices.d.ts.map +1 -0
  175. package/dist/runtime/personal-notices.js +137 -0
  176. package/dist/runtime/personal-notices.js.map +1 -0
  177. package/dist/runtime/personal-turns.d.ts +23 -0
  178. package/dist/runtime/personal-turns.d.ts.map +1 -0
  179. package/dist/runtime/personal-turns.js +51 -0
  180. package/dist/runtime/personal-turns.js.map +1 -0
  181. package/dist/runtime/principal-access.d.ts +8 -0
  182. package/dist/runtime/principal-access.d.ts.map +1 -1
  183. package/dist/runtime/principal-access.js +69 -2
  184. package/dist/runtime/principal-access.js.map +1 -1
  185. package/dist/runtime/principal-sessions.d.ts +16 -2
  186. package/dist/runtime/principal-sessions.d.ts.map +1 -1
  187. package/dist/runtime/principal-sessions.js +104 -21
  188. package/dist/runtime/principal-sessions.js.map +1 -1
  189. package/dist/runtime/skills.d.ts +86 -0
  190. package/dist/runtime/skills.d.ts.map +1 -0
  191. package/dist/runtime/skills.js +560 -0
  192. package/dist/runtime/skills.js.map +1 -0
  193. package/dist/runtime/stimulus-delivery.d.ts +21 -1
  194. package/dist/runtime/stimulus-delivery.d.ts.map +1 -1
  195. package/dist/runtime/stimulus-delivery.js +185 -18
  196. package/dist/runtime/stimulus-delivery.js.map +1 -1
  197. package/dist/runtime/tools-paths.d.ts +9 -0
  198. package/dist/runtime/tools-paths.d.ts.map +1 -0
  199. package/dist/runtime/tools-paths.js +98 -0
  200. package/dist/runtime/tools-paths.js.map +1 -0
  201. package/dist/runtime/turn-orders.d.ts +22 -2
  202. package/dist/runtime/turn-orders.d.ts.map +1 -1
  203. package/dist/runtime/turn-orders.js +60 -6
  204. package/dist/runtime/turn-orders.js.map +1 -1
  205. package/dist/storage/chat-sources.d.ts +21 -0
  206. package/dist/storage/chat-sources.d.ts.map +1 -1
  207. package/dist/storage/chat-sources.js +65 -3
  208. package/dist/storage/chat-sources.js.map +1 -1
  209. package/dist/tools/command-runner.d.ts +12 -0
  210. package/dist/tools/command-runner.d.ts.map +1 -0
  211. package/dist/tools/command-runner.js +65 -0
  212. package/dist/tools/command-runner.js.map +1 -0
  213. package/dist/tools/document-render.d.ts +24 -0
  214. package/dist/tools/document-render.d.ts.map +1 -0
  215. package/dist/tools/document-render.js +208 -0
  216. package/dist/tools/document-render.js.map +1 -0
  217. package/dist/tools/document-toolkit.d.ts +15 -0
  218. package/dist/tools/document-toolkit.d.ts.map +1 -0
  219. package/dist/tools/document-toolkit.js +501 -0
  220. package/dist/tools/document-toolkit.js.map +1 -0
  221. package/dist/tools/office-render.d.ts +28 -0
  222. package/dist/tools/office-render.d.ts.map +1 -0
  223. package/dist/tools/office-render.js +98 -0
  224. package/dist/tools/office-render.js.map +1 -0
  225. package/dist/tools/toolkit-process.d.ts +9 -0
  226. package/dist/tools/toolkit-process.d.ts.map +1 -0
  227. package/dist/tools/toolkit-process.js +148 -0
  228. package/dist/tools/toolkit-process.js.map +1 -0
  229. package/dist/tools/toolkit-state.d.ts +22 -0
  230. package/dist/tools/toolkit-state.d.ts.map +1 -0
  231. package/dist/tools/toolkit-state.js +116 -0
  232. package/dist/tools/toolkit-state.js.map +1 -0
  233. package/package.json +3 -2
  234. package/toolkit/inspect-office-zip.py +16 -0
  235. package/toolkit/rasterize.py +29 -0
  236. package/toolkit/requirements.lock +664 -0
  237. package/toolkit/smoke.py +32 -0
@@ -4,8 +4,14 @@ exports.readOwnerMemoryRecords = readOwnerMemoryRecords;
4
4
  exports.runtimeModelRun = runtimeModelRun;
5
5
  exports.guidanceInSearchOrder = guidanceInSearchOrder;
6
6
  exports.createOwnerRuntime = createOwnerRuntime;
7
+ const personal_jobs_js_1 = require("./personal-jobs.js");
8
+ const skills_js_1 = require("./skills.js");
9
+ const node_os_1 = require("node:os");
7
10
  const mama_core_1 = require("@jungjaehoon/mama-core");
11
+ const personal_notices_js_1 = require("./personal-notices.js");
12
+ const runtime_1 = require("@jungjaehoon/mama-core/runtime/runtime");
8
13
  const principal_sessions_js_1 = require("./principal-sessions.js");
14
+ const telegram_message_ledger_js_1 = require("../gateways/telegram-message-ledger.js");
9
15
  const member_session_js_1 = require("./member-session.js");
10
16
  const backend_security_js_1 = require("./backend-security.js");
11
17
  const member_paths_js_1 = require("./member-paths.js");
@@ -13,7 +19,9 @@ const principal_access_js_1 = require("./principal-access.js");
13
19
  const mama_core_2 = require("@jungjaehoon/mama-core");
14
20
  const node_crypto_1 = require("node:crypto");
15
21
  const node_path_1 = require("node:path");
22
+ const node_fs_1 = require("node:fs");
16
23
  const stored_source_reader_js_1 = require("../api/stored-source-reader.js");
24
+ const backend_security_js_2 = require("./backend-security.js");
17
25
  const report_persistence_js_1 = require("../api/report-persistence.js");
18
26
  const obsidian_writer_js_1 = require("../wiki/obsidian-writer.js");
19
27
  const source_archive_js_1 = require("../storage/source-archive.js");
@@ -84,7 +92,7 @@ function isOwnerGuidanceRecord(record) {
84
92
  return owner_authority_js_1.RULE_KINDS.includes(record.kind);
85
93
  }
86
94
  /** Active guidance among the search hits, in the search's own order. */
87
- function guidanceInSearchOrder(hitIds, records, limit, ownerRuleRecordIds) {
95
+ function guidanceInSearchOrder(hitIds, records, limit, ownerRuleRecordIds, skillScopes = []) {
88
96
  const active = new Map(records
89
97
  .filter((record) => isOwnerGuidanceRecord(record) && record.status === 'active')
90
98
  .map((record) => [record.id, record]));
@@ -98,6 +106,10 @@ function guidanceInSearchOrder(hitIds, records, limit, ownerRuleRecordIds) {
98
106
  topic: record.topic,
99
107
  summary: record.summary,
100
108
  ...(record.applies_when ? { appliesWhen: record.applies_when } : {}),
109
+ ...(record.kind === 'workflow' &&
110
+ record.scopes.some((scope) => skillScopes.some((admitted) => admitted.kind === scope.kind && admitted.id === scope.id))
111
+ ? { skillId: record.id }
112
+ : {}),
101
113
  ownerRule: ownerRuleRecordIds.has(record.id),
102
114
  },
103
115
  ]
@@ -119,7 +131,15 @@ async function createOwnerRuntime(options) {
119
131
  let stopping = false;
120
132
  const sessions = (0, principal_sessions_js_1.createPrincipalSessions)(options.ownerPrincipalId, {
121
133
  onUncertain: options.onStimulusUncertain,
122
- onDead: options.onStimulusDead,
134
+ onDead: async (row, reason) => {
135
+ if ((0, personal_notices_js_1.personalNoticePayload)(row)) {
136
+ await actionSurface?.personalNotices.close(row);
137
+ const ledger = options.messageLedger?.();
138
+ if (ledger)
139
+ (0, telegram_message_ledger_js_1.retirePersonalTurnReceipt)(ledger, row.stimulusId);
140
+ }
141
+ await options.onStimulusDead?.(row, reason);
142
+ },
123
143
  }, (id) => {
124
144
  if (stopping)
125
145
  throw new Error('Runtime is stopping');
@@ -149,6 +169,15 @@ async function createOwnerRuntime(options) {
149
169
  rawStore = new source_archive_js_1.RawStore(options.rawPath);
150
170
  const chat = new chat_sources_js_1.ChatSources(rawStore, database.adapter, options.ownerPrincipalId, options.agentId);
151
171
  const sourceStore = rawStore;
172
+ // A member's personal copy of a host exchange; the common management copy is separate.
173
+ const writeMemberHostExchange = (id, { message, reply }) => {
174
+ const archive = new chat_sources_js_1.ChatSources(sourceStore, database.adapter, id, `member-agent:${id}`);
175
+ archive.saveOwnerMessage({ ...message, senderPrincipalId: id });
176
+ archive.saveReply({ ...reply, messageRef: message.id, author: 'host' });
177
+ };
178
+ // A member's chat archive: its personal records for a DM, the common records for a room
179
+ // (as its room session keeps them).
180
+ const memberChat = (id, room) => new chat_sources_js_1.ChatSources(sourceStore, database.adapter, room === undefined ? id : options.ownerPrincipalId, `member-agent:${id}`);
152
181
  const storedSourceReader = (0, stored_source_reader_js_1.createStoredSourceReader)({
153
182
  adapter: database.adapter,
154
183
  rawStore: () => sourceStore,
@@ -181,7 +210,41 @@ async function createOwnerRuntime(options) {
181
210
  })()
182
211
  : {};
183
212
  const surface = (0, action_surface_js_1.createActionSurface)({
213
+ commonChannelId: options.commonChannelId,
184
214
  runtimeRoot: options.runtimeRoot,
215
+ skillPaths: {
216
+ toolsRoot: options.toolsRoot,
217
+ workspace: (id) => {
218
+ if (id === options.ownerPrincipalId)
219
+ return options.workspaceDir;
220
+ if (memberRoot === undefined)
221
+ throw new Error('Member skills require member_root');
222
+ return (0, member_paths_js_1.memberPaths)(memberRoot, id).workspaceDir;
223
+ },
224
+ },
225
+ documentPorts: {
226
+ toolsRoot: options.toolsRoot,
227
+ workspaceDir: options.workspaceDir,
228
+ deniedReadPaths: [
229
+ (0, node_os_1.homedir)(),
230
+ ...(0, backend_security_js_2.credentialReadPaths)(options.runtimeRoot, options.codexHome, options.replayKeyFile),
231
+ ...(0, backend_security_js_1.ownerNativeDataReadPaths)(options),
232
+ ],
233
+ ...(memberRoot === undefined
234
+ ? {}
235
+ : {
236
+ principalPaths: (id) => {
237
+ sessions.get(id);
238
+ return members.get(id).activePaths();
239
+ },
240
+ // Linked document resources have the same private-data boundary as native tools.
241
+ principalDeniedReadPaths: (id) => {
242
+ sessions.get(id);
243
+ return members.get(id).activeDeniedReadPaths();
244
+ },
245
+ }),
246
+ },
247
+ memberDirectoryChange: sessions.exclusive,
185
248
  adapter: database.adapter,
186
249
  ...(options.outboundAttempts === undefined
187
250
  ? {}
@@ -194,14 +257,43 @@ async function createOwnerRuntime(options) {
194
257
  storedSourceReader,
195
258
  timeZone: options.timeZone,
196
259
  configPath: (0, node_path_1.join)(options.runtimeRoot, 'config.yaml'),
197
- isOwnerMessageTurn: (sourceMessageRef, principalId = options.ownerPrincipalId) => ownerMailbox?.readInput(sourceMessageRef, principalId)?.kind === 'owner_message',
260
+ isOwnerMessageTurn: (sourceMessageRef, principalId) => ownerMailbox?.readInput(sourceMessageRef, principalId)?.kind === 'owner_message',
261
+ recordManagementReceipt: ({ callerPrincipalId, operationId, action, input, result }) => {
262
+ const at = Date.now();
263
+ const ref = `host:management:${operationId}`;
264
+ chat.saveOwnerMessage({
265
+ id: ref,
266
+ channelKey: 'management',
267
+ ...(callerPrincipalId === undefined ? {} : { senderPrincipalId: callerPrincipalId }),
268
+ occurredAt: at,
269
+ text: action,
270
+ payload: { managementAction: action },
271
+ });
272
+ chat.saveReply({
273
+ messageRef: ref,
274
+ text: JSON.stringify({ action, input, result }),
275
+ occurredAt: at,
276
+ deliveryVerified: true,
277
+ author: 'host',
278
+ });
279
+ },
198
280
  // Without member_root no member can be served or set aside, so no change may be committed.
199
281
  ...(memberRoot === undefined
200
282
  ? {}
201
283
  : {
284
+ memberNotices: {
285
+ mailbox: () => ownerMailbox,
286
+ wake: () => {
287
+ // The recipient delivery prepares service outside the common turn.
288
+ void intakeRuntime
289
+ .drainOnce()
290
+ .catch((error) => options.onPersonalJobError?.(error));
291
+ },
292
+ },
202
293
  memberRecords: {
203
294
  root: memberRoot,
204
295
  isStopping: () => stopping,
296
+ isReady: () => options.deliveryReady?.() ?? true,
205
297
  resumeQueued: () => {
206
298
  void intakeRuntime
207
299
  .drainOnce()
@@ -217,7 +309,12 @@ async function createOwnerRuntime(options) {
217
309
  serve: (id) => {
218
310
  serveMember(id);
219
311
  },
312
+ writeHostExchange: (id, exchange) => writeMemberHostExchange(id, exchange),
220
313
  recordExchange: (id, kind, ref, text, deliveryVerified) => {
314
+ if (database.adapter
315
+ .prepare("SELECT 1 FROM connector_event_index WHERE source_connector='chat' AND source_entity_id=? AND memory_scope_id=?")
316
+ .get(`${ref}:reply`, id))
317
+ return;
221
318
  const memberChat = new chat_sources_js_1.ChatSources(sourceStore, database.adapter, id, `member-agent:${id}`);
222
319
  const at = Date.now();
223
320
  memberChat.saveOwnerMessage({
@@ -236,18 +333,33 @@ async function createOwnerRuntime(options) {
236
333
  },
237
334
  },
238
335
  memberLifecycle: {
336
+ mailbox: () => ownerMailbox,
337
+ isStopping: () => stopping,
338
+ isServed: (id) => members.has(id),
339
+ resumeQueued: () => {
340
+ void intakeRuntime
341
+ .drainOnce()
342
+ .catch((error) => console.error('[Member management] delivery failed', error));
343
+ },
344
+ turnChain: sessions.turnChain,
239
345
  resetSession: async (id) => {
240
346
  const member = members.get(id);
241
347
  if (!member)
242
348
  throw new Error(`Member session is not served: ${id}`);
243
349
  await member.native.resetSession(member.native.sessionKey);
244
350
  },
245
- cancelQueued: (id, reason) => ownerMailbox.cancelQueued(id, reason),
351
+ cancelQueued: (id, reason) => {
352
+ if (reason === 'member_suspend')
353
+ options.memberEnrollment?.cancelSelection?.(id);
354
+ return ownerMailbox.cancelQueued(id, reason);
355
+ },
246
356
  retire: (id) => retireMember(id),
247
357
  resume: async (id) => {
248
358
  // A suspended member may retain a session after an earlier stop/move failure.
249
359
  await retireMember(id);
250
- principals.resume(id, Date.now());
360
+ const resumedAt = Date.now();
361
+ await surface.personalJobs.resume(id, resumedAt);
362
+ principals.resume(id, resumedAt);
251
363
  try {
252
364
  serveMember(id);
253
365
  }
@@ -266,7 +378,14 @@ async function createOwnerRuntime(options) {
266
378
  ...options.memberEnrollment,
267
379
  memberRoot,
268
380
  serveMember: (id) => {
381
+ if (stopping)
382
+ return 'next_start';
383
+ // Serving recreates the member's paths; an erasure checks them against the
384
+ // delivered export and serves the member itself when it ends.
385
+ if (surface.memberRecords.excludePrincipals().includes(id))
386
+ return 'after_erasure';
269
387
  serveMember(id);
388
+ return undefined;
270
389
  },
271
390
  },
272
391
  }),
@@ -284,7 +403,7 @@ async function createOwnerRuntime(options) {
284
403
  downloadsDir: options.attachmentPorts?.downloadsDir ?? '',
285
404
  };
286
405
  sessions.get(principalId);
287
- return members.get(principalId).paths;
406
+ return members.get(principalId).activePaths();
288
407
  },
289
408
  },
290
409
  ...(options.driveDelivery === undefined
@@ -311,6 +430,7 @@ async function createOwnerRuntime(options) {
311
430
  model: options.model,
312
431
  workspaceDir: options.workspaceDir,
313
432
  runtimeRoot: options.runtimeRoot,
433
+ toolsRoot: options.toolsRoot,
314
434
  deniedReadPaths: (0, backend_security_js_1.ownerNativeDataReadPaths)(options),
315
435
  replayKeyFile: options.replayKeyFile,
316
436
  ...(options.sandboxNetworkProxy === undefined
@@ -343,9 +463,61 @@ async function createOwnerRuntime(options) {
343
463
  ...(options.onRecordOrderEvent === undefined ? {} : { onEvent: options.onRecordOrderEvent }),
344
464
  });
345
465
  delivery = (0, stimulus_delivery_js_1.createStimulusDelivery)({
346
- turnChain: sessions.turnChain,
466
+ skills: (0, skills_js_1.createSkillTurnContext)(() => surface.skills.list(options.ownerPrincipalId)),
467
+ turnChain: sessions.turnChain(options.ownerPrincipalId),
347
468
  backend: options.backend,
348
469
  timeZone: options.timeZone,
470
+ principalRoles: (principalId) => principals.listRoles(principalId).map(({ role }) => role),
471
+ principalDisplayName: (principalId) => principals.findById(principalId)?.displayName,
472
+ commonPrincipalId: options.ownerPrincipalId,
473
+ ownerMessageSenderActive: (id) => {
474
+ const person = principals.findById(id);
475
+ return (person?.kind === 'member' && person.status === 'active' && (0, owner_authority_js_1.holdsOwnerRole)(principals, id));
476
+ },
477
+ rerouteOwnerMessage: async (row, sender) => {
478
+ const person = principals.findById(sender);
479
+ if (person?.kind !== 'member' ||
480
+ person.status !== 'active' ||
481
+ !row.stimulusId.startsWith(`telegram:${row.channelKey}:`) ||
482
+ !options.roomChannels?.includes(row.channelKey))
483
+ return false;
484
+ await serveMemberOnLane(sender);
485
+ // The room session cannot read the common downloads; its files go to the room's own.
486
+ const payload = row.payload;
487
+ let input = payload.input;
488
+ if (input?.attachments?.length) {
489
+ const target = (0, node_path_1.join)(members.get(sender).roomSession(row.channelKey).paths.downloadsDir, 'telegram');
490
+ (0, node_fs_1.mkdirSync)(target, { recursive: true });
491
+ input = {
492
+ ...input,
493
+ attachments: input.attachments.map((attachment) => {
494
+ if (!('path' in attachment))
495
+ return attachment;
496
+ const path = (0, node_path_1.join)(target, (0, node_path_1.basename)(attachment.path));
497
+ (0, node_fs_1.copyFileSync)(attachment.path, path);
498
+ return { ...attachment, path };
499
+ }),
500
+ };
501
+ }
502
+ // A deterministic handoff ref survives a crash between this append and the common
503
+ // acknowledgement. Mailbox ref identities are global; keep the evidence handles.
504
+ intakeRuntime.accept({
505
+ id: row.stimulusId,
506
+ kind: 'owner_message',
507
+ principalId: sender,
508
+ channelKey: row.channelKey,
509
+ occurredAt: row.occurredAt,
510
+ refs: row.refs.map((ref) => ({ ...ref, refId: `room-handoff:${sender}:${ref.refId}` })),
511
+ replyTo: row.replyTo,
512
+ payload: {
513
+ ...row.payload,
514
+ ...(input === undefined ? {} : { input }),
515
+ senderPrincipalId: sender,
516
+ roomChannel: row.channelKey,
517
+ },
518
+ });
519
+ return true;
520
+ },
349
521
  wikiEnabled: options.wiki?.enabled ?? false,
350
522
  formattingRoutes: options.formattingRoutes ?? {
351
523
  reports: 'telegram',
@@ -358,7 +530,13 @@ async function createOwnerRuntime(options) {
358
530
  ? {}
359
531
  : { onUncertain: options.onStimulusUncertain }),
360
532
  ...(options.onStimulusDead === undefined ? {} : { onDead: options.onStimulusDead }),
361
- ...(options.onStimulusSkipped === undefined ? {} : { onSkipped: options.onStimulusSkipped }),
533
+ onSkipped: async (row, reason) => {
534
+ if (row.kind === 'owner_message')
535
+ database.adapter
536
+ .prepare('UPDATE mailbox_inputs SET last_error = ? WHERE id = ?')
537
+ .run(reason, row.id);
538
+ await options.onStimulusSkipped?.(row, reason);
539
+ },
362
540
  ...(options.closeUncertain === undefined ? {} : { closeUncertain: options.closeUncertain }),
363
541
  sessionStart: (row) => (0, session_start_context_js_1.readSessionStartInput)({
364
542
  exchanges: chat.recentExchanges(row.stimulusId),
@@ -403,7 +581,7 @@ async function createOwnerRuntime(options) {
403
581
  });
404
582
  // Ten in search order: the session filter drops lessons already shown and keeps three.
405
583
  const hitIds = hits.map((hit) => hit.id);
406
- return guidanceInSearchOrder(hitIds, active, 10, (0, owner_authority_js_1.ownerRuleIds)(database.adapter, hitIds, options.ownerPrincipalId));
584
+ return guidanceInSearchOrder(hitIds, active, 10, (0, owner_authority_js_1.ownerRuleIds)(database.adapter, hitIds, options.ownerPrincipalId), surface.skills.workflowScopes(options.ownerPrincipalId));
407
585
  }),
408
586
  recordOrders,
409
587
  ...(options.onOwnerResult === undefined ? {} : { onOwnerResult: options.onOwnerResult }),
@@ -425,12 +603,14 @@ async function createOwnerRuntime(options) {
425
603
  throw new Error(`serveMember requires an active member: ${principalId}`);
426
604
  const member = (0, member_session_js_1.createMemberSession)(principalId, memberRoot, {
427
605
  options,
606
+ personalJobs: surface.personalJobs,
607
+ ownerPolicyProvider,
428
608
  ownerDeniedPaths,
429
609
  registeredMemberIds: () => principals.listMembers().map((row) => row.principalId),
430
610
  database,
431
611
  rawStore: sourceStore,
432
612
  surface,
433
- turnChain: sessions.turnChain,
613
+ turnChain: sessions.turnChain(principalId),
434
614
  readResult: (row) => row.nativeDelivery?.receipt
435
615
  ? intakeRuntime.mailbox.nativeInputs.resultForReceipt(row.nativeDelivery.receipt, row.principalId)
436
616
  : null,
@@ -440,16 +620,22 @@ async function createOwnerRuntime(options) {
440
620
  return member;
441
621
  };
442
622
  for (const member of activeMembers)
443
- prepareMember(member.principalId);
444
- const executionAccess = (principalId) => principalId === options.ownerPrincipalId
623
+ if (!surface.memberRecords.isBlocked(member.principalId))
624
+ prepareMember(member.principalId);
625
+ const executionAccess = (principalId, sessionKey) => principalId === options.ownerPrincipalId
445
626
  ? access
446
627
  : (0, principal_access_js_1.resolvePrincipalAccess)(principalId, {
447
628
  adapter: database.adapter,
448
629
  ownerAccess: access,
449
630
  agentId: `member-agent:${principalId}`,
631
+ sessionKey,
632
+ commonChannelId: options.commonChannelId,
450
633
  });
451
634
  const socketDispatch = Object.assign(async (...[call, context]) => {
452
- context = { ...context, access: executionAccess(context.access.principalId) };
635
+ context = {
636
+ ...context,
637
+ access: executionAccess(context.access.principalId, members.get(context.access.principalId)?.activeSessionKey()),
638
+ };
453
639
  const caller = context.session?.nativeCaller;
454
640
  if (caller !== undefined) {
455
641
  const principalSession = sessions.get(context.access.principalId).native;
@@ -476,6 +662,7 @@ async function createOwnerRuntime(options) {
476
662
  executionAccess(callerAccess.principalId);
477
663
  const ceiling = sessions.replaySourceEndMs(callerAccess.principalId);
478
664
  return {
665
+ ...members.get(callerAccess.principalId)?.activeSessionFacts(),
479
666
  ...(ceiling === undefined ? {} : { replaySourceEndMs: ceiling }),
480
667
  ...(request.session?.nativeCaller === undefined
481
668
  ? {}
@@ -486,8 +673,80 @@ async function createOwnerRuntime(options) {
486
673
  nativeSession: sessions.native,
487
674
  delivery: {
488
675
  ...sessions.delivery,
676
+ deliverHost: surface.deliverManagement,
677
+ prepareNative: async (row) => {
678
+ const payload = row.payload;
679
+ if (row.kind === 'owner_message' && payload?.deferredMemberMessage) {
680
+ if (!intakeRuntime.servesPrincipal(row.principalId))
681
+ await serveMemberOnLane(row.principalId);
682
+ // A room message belongs to its room session's common archive, never to the
683
+ // member's personal records.
684
+ const room = payload.roomChannel;
685
+ const archive = memberChat(row.principalId, room);
686
+ const existing = database.adapter
687
+ .prepare("SELECT current_observation_id, metadata_json FROM connector_event_index WHERE source_connector='chat' AND source_id=? AND memory_scope_id=?")
688
+ .get(row.stimulusId, room === undefined ? row.principalId : options.ownerPrincipalId);
689
+ let observation;
690
+ let input = payload.input;
691
+ if (existing) {
692
+ observation = existing.current_observation_id;
693
+ input = JSON.parse(existing.metadata_json).input ?? undefined;
694
+ }
695
+ else {
696
+ let message = {
697
+ id: row.stimulusId,
698
+ channelKey: row.channelKey,
699
+ occurredAt: row.occurredAt,
700
+ text: payload.text ?? '',
701
+ replyTo: row.replyTo,
702
+ ...(payload.input === undefined ? {} : { payload: payload.input }),
703
+ ...(payload.senderPrincipalId === undefined
704
+ ? {}
705
+ : { senderPrincipalId: payload.senderPrincipalId }),
706
+ };
707
+ if (message.payload &&
708
+ typeof message.payload === 'object' &&
709
+ !Array.isArray(message.payload) &&
710
+ 'pendingTelegramMessage' in message.payload) {
711
+ if (!options.prepareDeferredMemberInput)
712
+ throw new Error('Deferred Telegram input requires its gateway preparer');
713
+ message = await options.prepareDeferredMemberInput(row.principalId, message);
714
+ }
715
+ observation = archive.saveOwnerMessage(message);
716
+ input = message.payload;
717
+ }
718
+ intakeRuntime.mailbox.bindInputObservation(row.id, row.stimulusId, observation);
719
+ for (const ref of row.refs)
720
+ if (ref.refId === row.stimulusId)
721
+ ref.observationRef = observation;
722
+ // The immutable accepted payload remains the dedupe identity. Native delivery sees
723
+ // the stored projection's prepared attachments rather than downloading them again.
724
+ row.payload = { ...payload, ...(input === undefined ? {} : { input }) };
725
+ }
726
+ if ((0, personal_notices_js_1.personalNoticePayload)(row)) {
727
+ if (principals.findById(row.principalId)?.status !== 'active')
728
+ throw new runtime_1.StimulusQuarantine('Personal notice person is no longer active');
729
+ // Core calls this port before native admission, including scheduled inputs.
730
+ // Serve-on-demand may wait here, in this recipient's delivery, never in the common
731
+ // action. The one durable notice remains claimed without a native dispatch meanwhile.
732
+ try {
733
+ if (!intakeRuntime.servesPrincipal(row.principalId))
734
+ await serveMemberOnLane(row.principalId);
735
+ }
736
+ catch (error) {
737
+ // Suspend/offboard can finish while service waits for the directory lane.
738
+ // That lifecycle cancellation is final; an active person's preparation error retries.
739
+ if (principals.findById(row.principalId)?.status !== 'active')
740
+ throw new runtime_1.StimulusQuarantine('Personal notice person is no longer active');
741
+ throw error;
742
+ }
743
+ }
744
+ },
745
+ hostReady: () => !stopping,
746
+ excludePrincipals: surface.memberRecords.excludePrincipals,
489
747
  onSettled: surface.memberRecords.onSettled,
490
748
  onDead: (row, reason) => {
749
+ surface.onManagementDead(row, reason);
491
750
  surface.memberRecords.onDead(row, reason);
492
751
  return sessions.delivery.onDead?.(row, reason);
493
752
  },
@@ -495,27 +754,75 @@ async function createOwnerRuntime(options) {
495
754
  surface.memberRecords.onUncertain(row, reason);
496
755
  return sessions.delivery.onUncertain?.(row, reason);
497
756
  },
498
- ready: () => !stopping && !surface.memberRecords.isBusy() && (options.deliveryReady?.() ?? true),
757
+ ready: () => !stopping && (options.deliveryReady?.() ?? true),
499
758
  },
500
759
  reclaimStaleSocket: true,
501
760
  });
502
761
  ownerMailbox = intakeRuntime.mailbox;
503
762
  recordOrders.recover();
763
+ // Recovered host inputs need no new Telegram update to run.
764
+ void intakeRuntime
765
+ .drainOnce()
766
+ .catch((error) => console.error('[Member management] recovery failed', error));
504
767
  const intake = (0, stimulus_delivery_js_1.createStimulusIntake)(intakeRuntime, options.ownerPrincipalId, chat);
505
- const createMemberIntake = (id, member) => (0, stimulus_delivery_js_1.createStimulusIntake)(intakeRuntime, id, {
506
- saveOwnerMessage: (input) => {
507
- executionAccess(id);
508
- if (!intakeRuntime.servesPrincipal(id))
509
- throw new Error(`Member session is not served: ${id}`);
510
- return member.chat.saveOwnerMessage(input);
511
- },
512
- saveReply: (input) => member.chat.saveReply(input),
513
- });
768
+ const createMemberIntake = (id, member, room) => {
769
+ const archive = member?.chat ?? memberChat(id, room);
770
+ // A room intake made while the member was retired stays a room intake after the erasure
771
+ // ends: its message runs in the room session and is kept in the room's common archive.
772
+ const direct = (0, stimulus_delivery_js_1.createStimulusIntake)(intakeRuntime, id, {
773
+ saveOwnerMessage: (input) => {
774
+ executionAccess(id);
775
+ if (!intakeRuntime.servesPrincipal(id))
776
+ throw new Error(`Member session is not served: ${id}`);
777
+ return archive.saveOwnerMessage(input);
778
+ },
779
+ saveReply: (input) => archive.saveReply(input),
780
+ hasOwnerMessage: (ref) => archive.hasOwnerMessage(ref),
781
+ }, room);
782
+ const deferred = () => surface.memberRecords.isBlocked(id) && !intakeRuntime.servesPrincipal(id);
783
+ return {
784
+ ...direct,
785
+ deferIncoming: deferred,
786
+ acceptOwnerMessage: (input) => {
787
+ if (stopping)
788
+ throw new Error('Runtime is stopping');
789
+ if (!deferred())
790
+ return direct.acceptOwnerMessage(input);
791
+ executionAccess(id);
792
+ // New work received after retirement is durable, but creates no personal projection
793
+ // while an idempotent erasure is finishing its already exported snapshot.
794
+ const rowId = intakeRuntime.mailbox.enqueue({
795
+ id: input.id,
796
+ kind: 'owner_message',
797
+ principalId: id,
798
+ channelKey: input.channelKey,
799
+ occurredAt: input.occurredAt,
800
+ refs: [{ refId: input.id, observationRef: null }],
801
+ replyTo: input.replyTo,
802
+ payload: {
803
+ text: input.text,
804
+ deferredMemberMessage: true,
805
+ // A room message waits with its room, so it runs in that room session.
806
+ ...(room === undefined ? {} : { roomChannel: room }),
807
+ ...(input.payload === undefined ? {} : { input: input.payload }),
808
+ ...(input.senderPrincipalId === undefined
809
+ ? {}
810
+ : { senderPrincipalId: input.senderPrincipalId }),
811
+ },
812
+ });
813
+ return {
814
+ inputId: rowId === null ? null : String(rowId),
815
+ state: rowId === null ? 'duplicate' : 'accepted',
816
+ };
817
+ },
818
+ };
819
+ };
514
820
  for (const [id, member] of members)
515
821
  memberIntakes.set(id, createMemberIntake(id, member));
516
822
  const retireMember = async (id, setAside = true) => {
517
823
  if (memberRoot === undefined)
518
824
  throw new Error('Member lifecycle requires member_root');
825
+ surface.releasePrincipal(id);
519
826
  intakeRuntime.unservePrincipal(id);
520
827
  const member = members.get(id);
521
828
  if (member) {
@@ -565,7 +872,53 @@ async function createOwnerRuntime(options) {
565
872
  memberIntakes.set(principalId, memberIntake);
566
873
  return memberIntake;
567
874
  };
875
+ // Gateway DMs and personal jobs serve active unserved members through the directory lane. A
876
+ // served intake needs no directory work; P9 restores its own known paths on the member chain.
877
+ const serveMemberOnLane = (id) => {
878
+ executionAccess(id);
879
+ if (stopping)
880
+ return Promise.reject(new Error('Cannot serve a member after runtime stop'));
881
+ if (surface.memberRecords.isBlocked(id) && !intakeRuntime.servesPrincipal(id))
882
+ return Promise.resolve(createMemberIntake(id));
883
+ if (intakeRuntime.servesPrincipal(id))
884
+ return Promise.resolve(memberIntakes.get(id));
885
+ return sessions.exclusive(async () => {
886
+ if (stopping)
887
+ throw new Error('Cannot serve a member after runtime stop');
888
+ if (surface.memberRecords.isBlocked(id) && !intakeRuntime.servesPrincipal(id))
889
+ return createMemberIntake(id);
890
+ return serveMember(id);
891
+ });
892
+ };
893
+ const personalJobs = (0, personal_jobs_js_1.createPersonalJobScheduler)({
894
+ adapter: database.adapter,
895
+ store: surface.personalJobs,
896
+ mailbox: intakeRuntime.mailbox,
897
+ intake: serveMemberOnLane,
898
+ blocked: surface.memberRecords.isBlocked,
899
+ onError: (error) => options.onPersonalJobError?.(error),
900
+ });
901
+ let schedulerStop;
902
+ const beginStop = () => {
903
+ if (stopping)
904
+ return;
905
+ stopping = true;
906
+ schedulerStop = personalJobs.stop();
907
+ recordOrders.stop();
908
+ surface.memberRecords.stopRecovery();
909
+ intakeRuntime.beginStop();
910
+ };
911
+ let drainPromise;
912
+ const drain = () => (drainPromise ??= (async () => {
913
+ beginStop();
914
+ await intakeRuntime.stop();
915
+ await schedulerStop;
916
+ await surface.memberRecords.idle();
917
+ await sessions.idle();
918
+ })());
919
+ surface.memberRecords.startRecovery();
568
920
  return {
921
+ personalJobs,
569
922
  runtime: intakeRuntime,
570
923
  database,
571
924
  knowledge,
@@ -575,14 +928,31 @@ async function createOwnerRuntime(options) {
575
928
  wikiRoot,
576
929
  intake,
577
930
  acceptSourceDelta: intake.acceptSourceDelta,
578
- serveMember: (id) => {
931
+ serveMember: serveMemberOnLane,
932
+ memberIntake: (id, room) => {
579
933
  // Erasure owns restoration once it unserves the member. Intake callers must not
580
934
  // recreate a writer while the host takes and delivers that member's snapshot.
581
935
  if (surface.memberRecords.isBlocked(id) && !intakeRuntime.servesPrincipal(id))
936
+ return createMemberIntake(id, undefined, room);
937
+ if (stopped)
938
+ throw new Error('Cannot obtain member intake after runtime stop');
939
+ executionAccess(id);
940
+ const intake = memberIntakes.get(id);
941
+ if (!intake || !intakeRuntime.servesPrincipal(id))
582
942
  throw new Error(`Member session is not served: ${id}`);
583
- return serveMember(id);
943
+ if (room === undefined)
944
+ return intake;
945
+ const session = members.get(id).roomSession(room);
946
+ return (0, stimulus_delivery_js_1.createStimulusIntake)(intakeRuntime, id, {
947
+ saveOwnerMessage: (input) => {
948
+ executionAccess(id, session.native.sessionKey);
949
+ return session.chat.saveOwnerMessage(input);
950
+ },
951
+ saveReply: (input) => session.chat.saveReply(input),
952
+ hasOwnerMessage: (ref) => session.chat.hasOwnerMessage(ref),
953
+ }, room);
584
954
  },
585
- principalPaths: (principalId) => {
955
+ principalPaths: (principalId, room) => {
586
956
  if (principalId === options.ownerPrincipalId) {
587
957
  const downloadsDir = options.attachmentPorts?.downloadsDir;
588
958
  if (!downloadsDir?.trim())
@@ -594,40 +964,53 @@ async function createOwnerRuntime(options) {
594
964
  }
595
965
  executionAccess(principalId);
596
966
  sessions.get(principalId);
597
- return members.get(principalId).paths;
967
+ const member = members.get(principalId);
968
+ return room === undefined ? member.paths : member.roomSession(room).paths;
598
969
  },
599
- completeMemberEnrollment: (selection) => sessions.turnChain(async () => {
600
- if (stopped)
601
- throw new Error('Cannot enroll a member after runtime stop');
970
+ completeMemberEnrollment: (selection) => {
971
+ if (stopping)
972
+ return Promise.reject(new Error('Cannot enroll a member after runtime stop'));
602
973
  return surface.completeMemberEnrollment(selection);
603
- }),
974
+ },
604
975
  recordHostExchange: ({ message, reply }) => {
605
976
  const person = message.id.startsWith('telegram:')
606
977
  ? principals.resolveByExternal('telegram', 'private', message.channelKey)
607
978
  : null;
608
- const archive = person?.kind === 'member' && person.status === 'active'
609
- ? (serveMember(person.principalId), members.get(person.principalId).chat)
610
- : chat;
611
- archive.saveOwnerMessage({
612
- ...message,
613
- ...(person ? { senderPrincipalId: person.principalId } : {}),
614
- });
615
- archive.saveReply({ ...reply, messageRef: message.id, author: 'host' });
979
+ if (person?.kind !== 'member') {
980
+ chat.saveOwnerMessage({
981
+ ...message,
982
+ ...(person ? { senderPrincipalId: person.principalId } : {}),
983
+ });
984
+ chat.saveReply({ ...reply, messageRef: message.id, author: 'host' });
985
+ }
986
+ else {
987
+ // A member being erased gets its personal copy after the erasure, never inside it.
988
+ if (!surface.memberRecords.deferWhileErasing(person.principalId, { message, reply }))
989
+ writeMemberHostExchange(person.principalId, { message, reply });
990
+ const ref = `host:management:${message.id}`;
991
+ chat.saveOwnerMessage({
992
+ ...message,
993
+ id: ref,
994
+ channelKey: 'management',
995
+ senderPrincipalId: person.principalId,
996
+ });
997
+ chat.saveReply({ ...reply, messageRef: ref, author: 'host' });
998
+ }
616
999
  },
617
1000
  stop: () => {
618
1001
  if (stopPromise)
619
1002
  return stopPromise;
620
- stopping = true;
1003
+ beginStop();
621
1004
  stopPromise = (async () => {
622
- await surface.memberRecords.idle();
1005
+ await drain();
623
1006
  stopped = true;
624
- recordOrders.stop();
625
- await intakeRuntime.stop();
626
1007
  rawStore?.close();
627
1008
  await database.close();
628
1009
  })();
629
1010
  return stopPromise;
630
1011
  },
1012
+ beginStop,
1013
+ drain,
631
1014
  };
632
1015
  }
633
1016
  catch (error) {