@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.
- package/CHANGELOG.md +112 -0
- package/README.md +1 -1
- package/dist/api/attachment-actions.d.ts.map +1 -1
- package/dist/api/attachment-actions.js +8 -7
- package/dist/api/attachment-actions.js.map +1 -1
- package/dist/api/code-act-actions.d.ts +3 -1
- package/dist/api/code-act-actions.d.ts.map +1 -1
- package/dist/api/code-act-actions.js +21 -9
- package/dist/api/code-act-actions.js.map +1 -1
- package/dist/api/document-actions.d.ts +13 -0
- package/dist/api/document-actions.d.ts.map +1 -0
- package/dist/api/document-actions.js +55 -0
- package/dist/api/document-actions.js.map +1 -0
- package/dist/api/drive-actions.d.ts +4 -0
- package/dist/api/drive-actions.d.ts.map +1 -1
- package/dist/api/drive-actions.js +15 -7
- package/dist/api/drive-actions.js.map +1 -1
- package/dist/api/drive-delivery.d.ts.map +1 -1
- package/dist/api/drive-delivery.js +8 -3
- package/dist/api/drive-delivery.js.map +1 -1
- package/dist/api/member-enrollment.d.ts +9 -3
- package/dist/api/member-enrollment.d.ts.map +1 -1
- package/dist/api/member-enrollment.js +64 -16
- package/dist/api/member-enrollment.js.map +1 -1
- package/dist/api/member-export.d.ts.map +1 -1
- package/dist/api/member-export.js +14 -6
- package/dist/api/member-export.js.map +1 -1
- package/dist/api/member-lifecycle.d.ts +25 -4
- package/dist/api/member-lifecycle.d.ts.map +1 -1
- package/dist/api/member-lifecycle.js +88 -21
- package/dist/api/member-lifecycle.js.map +1 -1
- package/dist/api/member-notice-actions.d.ts +18 -0
- package/dist/api/member-notice-actions.d.ts.map +1 -0
- package/dist/api/member-notice-actions.js +184 -0
- package/dist/api/member-notice-actions.js.map +1 -0
- package/dist/api/member-records.d.ts +17 -2
- package/dist/api/member-records.d.ts.map +1 -1
- package/dist/api/member-records.js +440 -82
- package/dist/api/member-records.js.map +1 -1
- package/dist/api/member-share-actions.d.ts +3 -3
- package/dist/api/member-share-actions.d.ts.map +1 -1
- package/dist/api/member-share-actions.js +47 -20
- package/dist/api/member-share-actions.js.map +1 -1
- package/dist/api/owner-policy-actions.d.ts +1 -0
- package/dist/api/owner-policy-actions.d.ts.map +1 -1
- package/dist/api/owner-policy-actions.js +8 -4
- package/dist/api/owner-policy-actions.js.map +1 -1
- package/dist/api/owner-timezone-actions.d.ts +5 -2
- package/dist/api/owner-timezone-actions.d.ts.map +1 -1
- package/dist/api/owner-timezone-actions.js +4 -6
- package/dist/api/owner-timezone-actions.js.map +1 -1
- package/dist/api/personal-job-actions.d.ts +6 -0
- package/dist/api/personal-job-actions.d.ts.map +1 -0
- package/dist/api/personal-job-actions.js +72 -0
- package/dist/api/personal-job-actions.js.map +1 -0
- package/dist/api/security-events.d.ts +6 -1
- package/dist/api/security-events.d.ts.map +1 -1
- package/dist/api/security-events.js +34 -15
- package/dist/api/security-events.js.map +1 -1
- package/dist/api/skill-actions.d.ts +5 -0
- package/dist/api/skill-actions.d.ts.map +1 -0
- package/dist/api/skill-actions.js +66 -0
- package/dist/api/skill-actions.js.map +1 -0
- package/dist/api/source-actions.d.ts.map +1 -1
- package/dist/api/source-actions.js +4 -2
- package/dist/api/source-actions.js.map +1 -1
- package/dist/api/stored-source-reader.d.ts.map +1 -1
- package/dist/api/stored-source-reader.js +9 -2
- package/dist/api/stored-source-reader.js.map +1 -1
- package/dist/api/trello-actions.d.ts.map +1 -1
- package/dist/api/trello-actions.js +8 -2
- package/dist/api/trello-actions.js.map +1 -1
- package/dist/api/work-actions.d.ts +12 -2
- package/dist/api/work-actions.d.ts.map +1 -1
- package/dist/api/work-actions.js +57 -10
- package/dist/api/work-actions.js.map +1 -1
- package/dist/cli/commands/daemon.d.ts +12 -2
- package/dist/cli/commands/daemon.d.ts.map +1 -1
- package/dist/cli/commands/daemon.js +85 -13
- package/dist/cli/commands/daemon.js.map +1 -1
- package/dist/cli/commands/init.d.ts +4 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +23 -0
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/tools.d.ts +12 -0
- package/dist/cli/commands/tools.d.ts.map +1 -0
- package/dist/cli/commands/tools.js +109 -0
- package/dist/cli/commands/tools.js.map +1 -0
- package/dist/cli/index.js +6 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/runtime/claude-caller-config.d.ts +3 -1
- package/dist/cli/runtime/claude-caller-config.d.ts.map +1 -1
- package/dist/cli/runtime/claude-caller-config.js +14 -2
- package/dist/cli/runtime/claude-caller-config.js.map +1 -1
- package/dist/connectors/framework/polling-scheduler.d.ts +6 -1
- package/dist/connectors/framework/polling-scheduler.d.ts.map +1 -1
- package/dist/connectors/framework/polling-scheduler.js +25 -3
- package/dist/connectors/framework/polling-scheduler.js.map +1 -1
- package/dist/connectors/framework/stored-index-read.d.ts +2 -2
- package/dist/connectors/framework/stored-index-read.d.ts.map +1 -1
- package/dist/connectors/framework/stored-index-read.js +10 -3
- package/dist/connectors/framework/stored-index-read.js.map +1 -1
- package/dist/gateways/base-gateway.d.ts +7 -0
- package/dist/gateways/base-gateway.d.ts.map +1 -1
- package/dist/gateways/base-gateway.js +24 -0
- package/dist/gateways/base-gateway.js.map +1 -1
- package/dist/gateways/discord.d.ts +4 -0
- package/dist/gateways/discord.d.ts.map +1 -1
- package/dist/gateways/discord.js +24 -6
- package/dist/gateways/discord.js.map +1 -1
- package/dist/gateways/slack.d.ts +3 -0
- package/dist/gateways/slack.d.ts.map +1 -1
- package/dist/gateways/slack.js +15 -2
- package/dist/gateways/slack.js.map +1 -1
- package/dist/gateways/telegram-errors.d.ts +3 -0
- package/dist/gateways/telegram-errors.d.ts.map +1 -1
- package/dist/gateways/telegram-errors.js +5 -0
- package/dist/gateways/telegram-errors.js.map +1 -1
- package/dist/gateways/telegram-message-ledger.d.ts +7 -0
- package/dist/gateways/telegram-message-ledger.d.ts.map +1 -1
- package/dist/gateways/telegram-message-ledger.js +27 -2
- package/dist/gateways/telegram-message-ledger.js.map +1 -1
- package/dist/gateways/telegram.d.ts +24 -6
- package/dist/gateways/telegram.d.ts.map +1 -1
- package/dist/gateways/telegram.js +258 -44
- package/dist/gateways/telegram.js.map +1 -1
- package/dist/gateways/turn-contract.d.ts +6 -0
- package/dist/gateways/turn-contract.d.ts.map +1 -1
- package/dist/runtime/action-surface.d.ts +28 -3
- package/dist/runtime/action-surface.d.ts.map +1 -1
- package/dist/runtime/action-surface.js +316 -34
- package/dist/runtime/action-surface.js.map +1 -1
- package/dist/runtime/backend-security.d.ts +4 -0
- package/dist/runtime/backend-security.d.ts.map +1 -1
- package/dist/runtime/backend-security.js +8 -1
- package/dist/runtime/backend-security.js.map +1 -1
- package/dist/runtime/claude-caller-hook.d.ts +14 -0
- package/dist/runtime/claude-caller-hook.d.ts.map +1 -1
- package/dist/runtime/claude-caller-hook.js +21 -1
- package/dist/runtime/claude-caller-hook.js.map +1 -1
- package/dist/runtime/config.d.ts +6 -0
- package/dist/runtime/config.d.ts.map +1 -1
- package/dist/runtime/config.js +24 -0
- package/dist/runtime/config.js.map +1 -1
- package/dist/runtime/connectors.js +1 -1
- package/dist/runtime/connectors.js.map +1 -1
- package/dist/runtime/egress-proxy.d.ts +3 -3
- package/dist/runtime/egress-proxy.d.ts.map +1 -1
- package/dist/runtime/egress-proxy.js +16 -3
- package/dist/runtime/egress-proxy.js.map +1 -1
- package/dist/runtime/member-paths.d.ts +9 -1
- package/dist/runtime/member-paths.d.ts.map +1 -1
- package/dist/runtime/member-paths.js +67 -5
- package/dist/runtime/member-paths.js.map +1 -1
- package/dist/runtime/member-session.d.ts +21 -21
- package/dist/runtime/member-session.d.ts.map +1 -1
- package/dist/runtime/member-session.js +297 -34
- package/dist/runtime/member-session.js.map +1 -1
- package/dist/runtime/member-system-prompt.d.ts +3 -0
- package/dist/runtime/member-system-prompt.d.ts.map +1 -1
- package/dist/runtime/member-system-prompt.js +30 -4
- package/dist/runtime/member-system-prompt.js.map +1 -1
- package/dist/runtime/native-session.d.ts +8 -0
- package/dist/runtime/native-session.d.ts.map +1 -1
- package/dist/runtime/native-session.js +105 -17
- package/dist/runtime/native-session.js.map +1 -1
- package/dist/runtime/outbound-attempts.d.ts +5 -2
- package/dist/runtime/outbound-attempts.d.ts.map +1 -1
- package/dist/runtime/outbound-attempts.js +3 -2
- package/dist/runtime/outbound-attempts.js.map +1 -1
- package/dist/runtime/owner-authority.d.ts +16 -14
- package/dist/runtime/owner-authority.d.ts.map +1 -1
- package/dist/runtime/owner-authority.js +163 -7
- package/dist/runtime/owner-authority.js.map +1 -1
- package/dist/runtime/owner-runtime.d.ts +18 -5
- package/dist/runtime/owner-runtime.d.ts.map +1 -1
- package/dist/runtime/owner-runtime.js +429 -43
- package/dist/runtime/owner-runtime.js.map +1 -1
- package/dist/runtime/owner-system-prompt.d.ts +3 -1
- package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
- package/dist/runtime/owner-system-prompt.js +16 -6
- package/dist/runtime/owner-system-prompt.js.map +1 -1
- package/dist/runtime/personal-job-cron.d.ts +6 -0
- package/dist/runtime/personal-job-cron.d.ts.map +1 -0
- package/dist/runtime/personal-job-cron.js +154 -0
- package/dist/runtime/personal-job-cron.js.map +1 -0
- package/dist/runtime/personal-jobs.d.ts +107 -0
- package/dist/runtime/personal-jobs.d.ts.map +1 -0
- package/dist/runtime/personal-jobs.js +353 -0
- package/dist/runtime/personal-jobs.js.map +1 -0
- package/dist/runtime/personal-notices.d.ts +38 -0
- package/dist/runtime/personal-notices.d.ts.map +1 -0
- package/dist/runtime/personal-notices.js +137 -0
- package/dist/runtime/personal-notices.js.map +1 -0
- package/dist/runtime/personal-turns.d.ts +23 -0
- package/dist/runtime/personal-turns.d.ts.map +1 -0
- package/dist/runtime/personal-turns.js +51 -0
- package/dist/runtime/personal-turns.js.map +1 -0
- package/dist/runtime/principal-access.d.ts +8 -0
- package/dist/runtime/principal-access.d.ts.map +1 -1
- package/dist/runtime/principal-access.js +70 -2
- package/dist/runtime/principal-access.js.map +1 -1
- package/dist/runtime/principal-sessions.d.ts +16 -2
- package/dist/runtime/principal-sessions.d.ts.map +1 -1
- package/dist/runtime/principal-sessions.js +104 -21
- package/dist/runtime/principal-sessions.js.map +1 -1
- package/dist/runtime/skills.d.ts +86 -0
- package/dist/runtime/skills.d.ts.map +1 -0
- package/dist/runtime/skills.js +560 -0
- package/dist/runtime/skills.js.map +1 -0
- package/dist/runtime/stimulus-delivery.d.ts +21 -1
- package/dist/runtime/stimulus-delivery.d.ts.map +1 -1
- package/dist/runtime/stimulus-delivery.js +185 -18
- package/dist/runtime/stimulus-delivery.js.map +1 -1
- package/dist/runtime/tools-paths.d.ts +9 -0
- package/dist/runtime/tools-paths.d.ts.map +1 -0
- package/dist/runtime/tools-paths.js +98 -0
- package/dist/runtime/tools-paths.js.map +1 -0
- package/dist/runtime/turn-orders.d.ts +22 -2
- package/dist/runtime/turn-orders.d.ts.map +1 -1
- package/dist/runtime/turn-orders.js +60 -6
- package/dist/runtime/turn-orders.js.map +1 -1
- package/dist/runtime/web-fetch-scope.d.ts +20 -0
- package/dist/runtime/web-fetch-scope.d.ts.map +1 -0
- package/dist/runtime/web-fetch-scope.js +68 -0
- package/dist/runtime/web-fetch-scope.js.map +1 -0
- package/dist/storage/chat-sources.d.ts +21 -0
- package/dist/storage/chat-sources.d.ts.map +1 -1
- package/dist/storage/chat-sources.js +65 -3
- package/dist/storage/chat-sources.js.map +1 -1
- package/dist/tools/command-runner.d.ts +12 -0
- package/dist/tools/command-runner.d.ts.map +1 -0
- package/dist/tools/command-runner.js +65 -0
- package/dist/tools/command-runner.js.map +1 -0
- package/dist/tools/document-render.d.ts +24 -0
- package/dist/tools/document-render.d.ts.map +1 -0
- package/dist/tools/document-render.js +208 -0
- package/dist/tools/document-render.js.map +1 -0
- package/dist/tools/document-toolkit.d.ts +15 -0
- package/dist/tools/document-toolkit.d.ts.map +1 -0
- package/dist/tools/document-toolkit.js +501 -0
- package/dist/tools/document-toolkit.js.map +1 -0
- package/dist/tools/office-render.d.ts +28 -0
- package/dist/tools/office-render.d.ts.map +1 -0
- package/dist/tools/office-render.js +101 -0
- package/dist/tools/office-render.js.map +1 -0
- package/dist/tools/toolkit-process.d.ts +9 -0
- package/dist/tools/toolkit-process.d.ts.map +1 -0
- package/dist/tools/toolkit-process.js +148 -0
- package/dist/tools/toolkit-process.js.map +1 -0
- package/dist/tools/toolkit-state.d.ts +22 -0
- package/dist/tools/toolkit-state.d.ts.map +1 -0
- package/dist/tools/toolkit-state.js +116 -0
- package/dist/tools/toolkit-state.js.map +1 -0
- package/package.json +3 -2
- package/toolkit/inspect-office-zip.py +16 -0
- package/toolkit/rasterize.py +29 -0
- package/toolkit/requirements.in +11 -0
- package/toolkit/requirements.lock +1643 -0
- 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.
|
|
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
|
-
//
|
|
93
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
|
|
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),
|