@geometra/mcp 1.64.0 → 1.65.1
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/README.md +29 -7
- package/dist/index.js +2 -5
- package/dist/proxy-spawn.d.ts +29 -0
- package/dist/proxy-spawn.js +49 -17
- package/dist/server.js +849 -219
- package/dist/session-state.js +262 -80
- package/dist/session.d.ts +93 -13
- package/dist/session.js +1365 -321
- package/dist/state-privacy.d.ts +23 -0
- package/dist/state-privacy.js +171 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +32 -0
- package/package.json +4 -4
package/dist/session-state.js
CHANGED
|
@@ -1,76 +1,248 @@
|
|
|
1
|
-
import { mkdirSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
1
|
+
import { accessSync, chmodSync, closeSync, constants, lstatSync, mkdirSync, openSync, } from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
|
-
import { threadId } from 'node:worker_threads';
|
|
5
3
|
import { ParallelMcpOrchestrator, SqliteParallelMcpStore, } from '@razroo/parallel-mcp';
|
|
4
|
+
import { sanitizeRetainedCode, sanitizeRetainedError, sanitizeRetainedState, } from './state-privacy.js';
|
|
6
5
|
const SESSION_NAMESPACE = 'geometra-mcp-session';
|
|
7
6
|
const SESSION_TASK_KEY = 'session.live';
|
|
8
7
|
const SESSION_LEASE_MS = 60_000;
|
|
9
8
|
const SESSION_SWEEP_MS = 15_000;
|
|
10
9
|
const SESSION_WORKER_PREFIX = `geometra-mcp:${process.pid}`;
|
|
11
|
-
|
|
10
|
+
const SECURE_DIRECTORY_MODE = 0o700;
|
|
11
|
+
const SECURE_FILE_MODE = 0o600;
|
|
12
|
+
class SessionStateConfigurationError extends Error {
|
|
13
|
+
}
|
|
14
|
+
let lifecycleRegistry = null;
|
|
15
|
+
function configurationError(message) {
|
|
16
|
+
return new SessionStateConfigurationError(`Refusing GEOMETRA_MCP_STATE_FILE: ${message}`);
|
|
17
|
+
}
|
|
18
|
+
function lstatIfPresent(targetPath, description) {
|
|
19
|
+
try {
|
|
20
|
+
return lstatSync(targetPath);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === 'ENOENT')
|
|
24
|
+
return null;
|
|
25
|
+
throw configurationError(`${description} could not be inspected securely`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function assertCurrentProcessOwns(stats, description) {
|
|
29
|
+
if (typeof process.geteuid === 'function' && stats.uid !== process.geteuid()) {
|
|
30
|
+
throw configurationError(`${description} must be owned by the current user`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function assertSecureStateDirectory(directory) {
|
|
34
|
+
const stats = lstatIfPresent(directory, 'parent directory');
|
|
35
|
+
if (!stats)
|
|
36
|
+
throw configurationError('parent directory does not exist');
|
|
37
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
38
|
+
throw configurationError('parent must be a real directory, not a symbolic link');
|
|
39
|
+
}
|
|
40
|
+
assertCurrentProcessOwns(stats, 'parent directory');
|
|
41
|
+
if ((stats.mode & 0o077) !== 0) {
|
|
42
|
+
throw configurationError('existing parent directory must not grant group or other access (mode 0700 or stricter)');
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
accessSync(directory, constants.R_OK | constants.W_OK | constants.X_OK);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw configurationError('parent directory must be readable, writable, and searchable by the current user');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function assertRegularOwnedArtifact(stats, description) {
|
|
52
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
53
|
+
throw configurationError(`${description} must be a regular file, not a symbolic link or special file`);
|
|
54
|
+
}
|
|
55
|
+
assertCurrentProcessOwns(stats, description);
|
|
56
|
+
if (stats.nlink !== 1) {
|
|
57
|
+
throw configurationError(`${description} must not have multiple hard links`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function secureExistingArtifact(artifactPath, description) {
|
|
61
|
+
const stats = lstatIfPresent(artifactPath, description);
|
|
62
|
+
if (!stats)
|
|
63
|
+
return false;
|
|
64
|
+
assertRegularOwnedArtifact(stats, description);
|
|
65
|
+
try {
|
|
66
|
+
chmodSync(artifactPath, SECURE_FILE_MODE);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw configurationError(`${description} permissions could not be restricted to mode 0600`);
|
|
70
|
+
}
|
|
71
|
+
const secured = lstatIfPresent(artifactPath, description);
|
|
72
|
+
if (!secured)
|
|
73
|
+
throw configurationError(`${description} disappeared while permissions were secured`);
|
|
74
|
+
assertRegularOwnedArtifact(secured, description);
|
|
75
|
+
if ((secured.mode & 0o077) !== 0) {
|
|
76
|
+
throw configurationError(`${description} permissions are not private`);
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
function sqliteSidecarPaths(filename) {
|
|
81
|
+
return [
|
|
82
|
+
[`${filename}-wal`, 'WAL file'],
|
|
83
|
+
[`${filename}-shm`, 'shared-memory file'],
|
|
84
|
+
[`${filename}-journal`, 'rollback journal'],
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
function preparePersistentStateFile(filename) {
|
|
88
|
+
const directory = path.dirname(filename);
|
|
89
|
+
if (!lstatIfPresent(directory, 'parent directory')) {
|
|
90
|
+
try {
|
|
91
|
+
mkdirSync(directory, { recursive: true, mode: SECURE_DIRECTORY_MODE });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw configurationError('parent directory could not be created securely');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
assertSecureStateDirectory(directory);
|
|
98
|
+
for (const [sidecarPath, description] of sqliteSidecarPaths(filename)) {
|
|
99
|
+
secureExistingArtifact(sidecarPath, description);
|
|
100
|
+
}
|
|
101
|
+
if (!secureExistingArtifact(filename, 'database file')) {
|
|
102
|
+
let descriptor;
|
|
103
|
+
try {
|
|
104
|
+
const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
105
|
+
descriptor = openSync(filename, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | noFollow, SECURE_FILE_MODE);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw configurationError('database file could not be created exclusively and securely');
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
if (descriptor !== undefined)
|
|
112
|
+
closeSync(descriptor);
|
|
113
|
+
}
|
|
114
|
+
secureExistingArtifact(filename, 'database file');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function securePersistentArtifacts(filename) {
|
|
118
|
+
assertSecureStateDirectory(path.dirname(filename));
|
|
119
|
+
if (!secureExistingArtifact(filename, 'database file')) {
|
|
120
|
+
throw configurationError('database file disappeared while storage was active');
|
|
121
|
+
}
|
|
122
|
+
for (const [sidecarPath, description] of sqliteSidecarPaths(filename)) {
|
|
123
|
+
secureExistingArtifact(sidecarPath, description);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function resolveSessionStateStorage() {
|
|
12
127
|
const raw = process.env.GEOMETRA_MCP_STATE_FILE?.trim();
|
|
13
|
-
if (raw) {
|
|
14
|
-
|
|
15
|
-
return raw;
|
|
128
|
+
if (!raw || raw === ':memory:') {
|
|
129
|
+
return { filename: ':memory:', persistentPath: null };
|
|
16
130
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
131
|
+
if (raw.includes('\0'))
|
|
132
|
+
throw configurationError('path contains an invalid null byte');
|
|
133
|
+
const filename = path.resolve(raw);
|
|
134
|
+
preparePersistentStateFile(filename);
|
|
135
|
+
return { filename, persistentPath: filename };
|
|
20
136
|
}
|
|
21
137
|
function isSessionLifecycleDisabled() {
|
|
22
138
|
const raw = process.env.GEOMETRA_MCP_DISABLE_SESSION_LIFECYCLE?.trim().toLowerCase();
|
|
23
139
|
return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
|
|
24
140
|
}
|
|
25
141
|
function formatSessionLifecycleInitError(error) {
|
|
26
|
-
if (error instanceof
|
|
27
|
-
if (error.code === 'ERR_DLOPEN_FAILED' &&
|
|
28
|
-
error.message.includes('better_sqlite3.node')) {
|
|
29
|
-
return `${error.message}. Rebuild the native module with \`npm rebuild better-sqlite3\` in the MCP package directory, or reinstall dependencies for the current Node.js version.`;
|
|
30
|
-
}
|
|
142
|
+
if (error instanceof SessionStateConfigurationError)
|
|
31
143
|
return error.message;
|
|
144
|
+
if (error instanceof Error &&
|
|
145
|
+
error.code === 'ERR_DLOPEN_FAILED' &&
|
|
146
|
+
error.message.includes('better_sqlite3.node')) {
|
|
147
|
+
return 'The SQLite native module is unavailable. Rebuild it with `npm rebuild better-sqlite3` in the MCP package directory, or reinstall dependencies for the current Node.js version.';
|
|
32
148
|
}
|
|
33
|
-
return
|
|
149
|
+
return 'Secure session lifecycle storage could not be initialized';
|
|
34
150
|
}
|
|
35
151
|
function createSessionLifecycleRegistry() {
|
|
36
152
|
if (isSessionLifecycleDisabled()) {
|
|
37
|
-
process.stderr.write('[geometra-mcp]
|
|
153
|
+
process.stderr.write('[geometra-mcp] session lifecycle disabled via GEOMETRA_MCP_DISABLE_SESSION_LIFECYCLE\n');
|
|
38
154
|
return {
|
|
39
155
|
available: false,
|
|
40
156
|
orchestrator: null,
|
|
157
|
+
secureArtifacts: () => { },
|
|
41
158
|
close: () => { },
|
|
42
159
|
};
|
|
43
160
|
}
|
|
161
|
+
const configuredStateFile = process.env.GEOMETRA_MCP_STATE_FILE?.trim();
|
|
162
|
+
const persistentStorageRequested = Boolean(configuredStateFile && configuredStateFile !== ':memory:');
|
|
163
|
+
let orchestrator = null;
|
|
44
164
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
165
|
+
const storage = resolveSessionStateStorage();
|
|
166
|
+
orchestrator = new ParallelMcpOrchestrator(new SqliteParallelMcpStore({ filename: storage.filename }), { defaultLeaseMs: SESSION_LEASE_MS });
|
|
167
|
+
const secureArtifacts = storage.persistentPath
|
|
168
|
+
? () => securePersistentArtifacts(storage.persistentPath)
|
|
169
|
+
: () => { };
|
|
170
|
+
secureArtifacts();
|
|
171
|
+
let closed = false;
|
|
172
|
+
let leaseSweep = null;
|
|
173
|
+
const close = () => {
|
|
174
|
+
if (closed)
|
|
175
|
+
return;
|
|
176
|
+
closed = true;
|
|
177
|
+
if (leaseSweep)
|
|
178
|
+
clearInterval(leaseSweep);
|
|
179
|
+
try {
|
|
180
|
+
secureArtifacts();
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
orchestrator?.close();
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
leaseSweep = setInterval(() => {
|
|
187
|
+
if (closed)
|
|
188
|
+
return;
|
|
47
189
|
try {
|
|
48
|
-
orchestrator
|
|
190
|
+
orchestrator?.expireLeases();
|
|
191
|
+
secureArtifacts();
|
|
49
192
|
}
|
|
50
193
|
catch {
|
|
51
|
-
|
|
194
|
+
process.stderr.write('[geometra-mcp] session lifecycle storage failed a background security check and was closed\n');
|
|
195
|
+
try {
|
|
196
|
+
close();
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
/* already failed closed */
|
|
200
|
+
}
|
|
52
201
|
}
|
|
53
202
|
}, SESSION_SWEEP_MS);
|
|
54
203
|
leaseSweep.unref();
|
|
55
204
|
return {
|
|
56
|
-
available
|
|
57
|
-
|
|
58
|
-
close: () => {
|
|
59
|
-
clearInterval(leaseSweep);
|
|
60
|
-
orchestrator.close();
|
|
205
|
+
get available() {
|
|
206
|
+
return !closed;
|
|
61
207
|
},
|
|
208
|
+
orchestrator,
|
|
209
|
+
secureArtifacts,
|
|
210
|
+
close,
|
|
62
211
|
};
|
|
63
212
|
}
|
|
64
213
|
catch (error) {
|
|
65
|
-
|
|
214
|
+
try {
|
|
215
|
+
orchestrator?.close();
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
/* initialization already failed */
|
|
219
|
+
}
|
|
220
|
+
const safeError = formatSessionLifecycleInitError(error);
|
|
221
|
+
if (persistentStorageRequested) {
|
|
222
|
+
// The caught cause can contain the configured path or native-module
|
|
223
|
+
// filesystem details; attaching it would defeat this privacy boundary.
|
|
224
|
+
// eslint-disable-next-line preserve-caught-error
|
|
225
|
+
throw new Error(safeError);
|
|
226
|
+
}
|
|
227
|
+
process.stderr.write(`[geometra-mcp] session lifecycle unavailable: ${safeError}\n`);
|
|
66
228
|
return {
|
|
67
229
|
available: false,
|
|
68
230
|
orchestrator: null,
|
|
231
|
+
secureArtifacts: () => { },
|
|
69
232
|
close: () => { },
|
|
70
233
|
};
|
|
71
234
|
}
|
|
72
235
|
}
|
|
73
|
-
|
|
236
|
+
function getSessionLifecycleRegistry() {
|
|
237
|
+
if (!lifecycleRegistry)
|
|
238
|
+
lifecycleRegistry = createSessionLifecycleRegistry();
|
|
239
|
+
return lifecycleRegistry;
|
|
240
|
+
}
|
|
241
|
+
function currentSessionLifecycleRegistry() {
|
|
242
|
+
return lifecycleRegistry?.available && lifecycleRegistry.orchestrator
|
|
243
|
+
? lifecycleRegistry
|
|
244
|
+
: null;
|
|
245
|
+
}
|
|
74
246
|
function extractPageUrl(target) {
|
|
75
247
|
const cached = target.cachedA11y?.meta?.pageUrl;
|
|
76
248
|
if (typeof cached === 'string' && cached.length > 0)
|
|
@@ -80,31 +252,18 @@ function extractPageUrl(target) {
|
|
|
80
252
|
return semantic.pageUrl;
|
|
81
253
|
return null;
|
|
82
254
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return value.toISOString();
|
|
91
|
-
}
|
|
92
|
-
if (Array.isArray(value)) {
|
|
93
|
-
return value.map(item => toJsonValue(item));
|
|
94
|
-
}
|
|
95
|
-
if (typeof value === 'object') {
|
|
96
|
-
const object = {};
|
|
97
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
98
|
-
if (entry === undefined)
|
|
99
|
-
continue;
|
|
100
|
-
object[key] = toJsonValue(entry);
|
|
101
|
-
}
|
|
102
|
-
return object;
|
|
255
|
+
function toRetainedJsonValue(value) {
|
|
256
|
+
return sanitizeRetainedState(value);
|
|
257
|
+
}
|
|
258
|
+
function toRetainedJsonObject(value) {
|
|
259
|
+
const retained = sanitizeRetainedState(value);
|
|
260
|
+
if (retained && typeof retained === 'object' && !Array.isArray(retained)) {
|
|
261
|
+
return retained;
|
|
103
262
|
}
|
|
104
|
-
return
|
|
263
|
+
return {};
|
|
105
264
|
}
|
|
106
265
|
function buildSessionContext(target, label, extra) {
|
|
107
|
-
return {
|
|
266
|
+
return toRetainedJsonObject({
|
|
108
267
|
sessionId: target.id,
|
|
109
268
|
label,
|
|
110
269
|
transportUrl: target.url,
|
|
@@ -116,8 +275,8 @@ function buildSessionContext(target, label, extra) {
|
|
|
116
275
|
hasLayout: target.layout !== null,
|
|
117
276
|
hasTree: target.tree !== null,
|
|
118
277
|
connectMode: target.connectTrace?.mode ?? null,
|
|
119
|
-
...(extra ? { extra
|
|
120
|
-
};
|
|
278
|
+
...(extra ? { extra } : {}),
|
|
279
|
+
});
|
|
121
280
|
}
|
|
122
281
|
function liveTaskIdFor(sessionId) {
|
|
123
282
|
return `${sessionId}:live`;
|
|
@@ -128,8 +287,23 @@ function liveTaskKindFor(sessionId) {
|
|
|
128
287
|
function workerIdFor(sessionId) {
|
|
129
288
|
return `${SESSION_WORKER_PREFIX}:${sessionId}`;
|
|
130
289
|
}
|
|
290
|
+
function secureAfterWrite(registry) {
|
|
291
|
+
try {
|
|
292
|
+
registry.secureArtifacts();
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
try {
|
|
296
|
+
registry.close();
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
/* fail closed even if SQLite close also fails */
|
|
300
|
+
}
|
|
301
|
+
throw new Error('Session lifecycle storage failed a security check and was closed');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
131
304
|
export function initializeSessionLifecycle(target, options) {
|
|
132
|
-
|
|
305
|
+
const registry = getSessionLifecycleRegistry();
|
|
306
|
+
if (!registry.available || !registry.orchestrator) {
|
|
133
307
|
target.lifecycleFinalized = true;
|
|
134
308
|
target.lifecycleTaskId = undefined;
|
|
135
309
|
target.lifecycleTaskKind = undefined;
|
|
@@ -137,7 +311,7 @@ export function initializeSessionLifecycle(target, options) {
|
|
|
137
311
|
target.lifecycleWorkerId = undefined;
|
|
138
312
|
return;
|
|
139
313
|
}
|
|
140
|
-
const orchestrator =
|
|
314
|
+
const orchestrator = registry.orchestrator;
|
|
141
315
|
const sessionId = target.id;
|
|
142
316
|
const taskId = liveTaskIdFor(sessionId);
|
|
143
317
|
const taskKind = liveTaskKindFor(sessionId);
|
|
@@ -145,7 +319,7 @@ export function initializeSessionLifecycle(target, options) {
|
|
|
145
319
|
orchestrator.createRun({
|
|
146
320
|
id: sessionId,
|
|
147
321
|
namespace: SESSION_NAMESPACE,
|
|
148
|
-
metadata:
|
|
322
|
+
metadata: toRetainedJsonValue({
|
|
149
323
|
transportMode: options?.transportMode ?? 'direct-ws',
|
|
150
324
|
isolated: target.isolated === true,
|
|
151
325
|
}),
|
|
@@ -158,7 +332,7 @@ export function initializeSessionLifecycle(target, options) {
|
|
|
158
332
|
runId: sessionId,
|
|
159
333
|
key: SESSION_TASK_KEY,
|
|
160
334
|
kind: taskKind,
|
|
161
|
-
input:
|
|
335
|
+
input: toRetainedJsonValue({
|
|
162
336
|
transportUrl: target.url,
|
|
163
337
|
requestedPageUrl: options?.pageUrl ?? null,
|
|
164
338
|
}),
|
|
@@ -169,13 +343,14 @@ export function initializeSessionLifecycle(target, options) {
|
|
|
169
343
|
leaseMs: SESSION_LEASE_MS,
|
|
170
344
|
});
|
|
171
345
|
if (!claimed || claimed.task.id !== taskId) {
|
|
172
|
-
throw new Error(
|
|
346
|
+
throw new Error('Failed to initialize session lifecycle task');
|
|
173
347
|
}
|
|
174
348
|
orchestrator.markTaskRunning({
|
|
175
349
|
taskId,
|
|
176
350
|
leaseId: claimed.lease.id,
|
|
177
351
|
workerId,
|
|
178
352
|
});
|
|
353
|
+
secureAfterWrite(registry);
|
|
179
354
|
target.lifecycleTaskId = taskId;
|
|
180
355
|
target.lifecycleTaskKind = taskKind;
|
|
181
356
|
target.lifecycleLeaseId = claimed.lease.id;
|
|
@@ -183,73 +358,80 @@ export function initializeSessionLifecycle(target, options) {
|
|
|
183
358
|
target.lifecycleFinalized = false;
|
|
184
359
|
}
|
|
185
360
|
export function heartbeatSessionLifecycle(target) {
|
|
186
|
-
|
|
361
|
+
const registry = currentSessionLifecycleRegistry();
|
|
362
|
+
if (!registry?.orchestrator)
|
|
187
363
|
return;
|
|
188
|
-
const orchestrator = lifecycleRegistry.orchestrator;
|
|
189
364
|
if (target.lifecycleFinalized || !target.lifecycleTaskId || !target.lifecycleLeaseId || !target.lifecycleWorkerId)
|
|
190
365
|
return;
|
|
191
|
-
orchestrator.heartbeatLease({
|
|
366
|
+
registry.orchestrator.heartbeatLease({
|
|
192
367
|
taskId: target.lifecycleTaskId,
|
|
193
368
|
leaseId: target.lifecycleLeaseId,
|
|
194
369
|
workerId: target.lifecycleWorkerId,
|
|
195
370
|
leaseMs: SESSION_LEASE_MS,
|
|
196
371
|
});
|
|
372
|
+
secureAfterWrite(registry);
|
|
197
373
|
}
|
|
198
374
|
export function recordSessionSnapshot(target, label, extra) {
|
|
199
|
-
|
|
375
|
+
const registry = currentSessionLifecycleRegistry();
|
|
376
|
+
if (!registry?.orchestrator || !target.lifecycleTaskId)
|
|
200
377
|
return;
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
return;
|
|
204
|
-
orchestrator.appendContextSnapshot({
|
|
378
|
+
const retainedLabel = sanitizeRetainedCode(label);
|
|
379
|
+
registry.orchestrator.appendContextSnapshot({
|
|
205
380
|
runId: target.id,
|
|
206
381
|
taskId: target.lifecycleTaskId,
|
|
207
382
|
scope: 'run',
|
|
208
|
-
label,
|
|
209
|
-
payload: buildSessionContext(target,
|
|
383
|
+
label: retainedLabel,
|
|
384
|
+
payload: buildSessionContext(target, retainedLabel, extra),
|
|
210
385
|
});
|
|
386
|
+
secureAfterWrite(registry);
|
|
211
387
|
}
|
|
212
388
|
export function completeSessionLifecycle(target, reason, extra) {
|
|
213
|
-
|
|
389
|
+
const registry = currentSessionLifecycleRegistry();
|
|
390
|
+
if (!registry?.orchestrator)
|
|
214
391
|
return;
|
|
215
|
-
const orchestrator = lifecycleRegistry.orchestrator;
|
|
216
392
|
if (target.lifecycleFinalized || !target.lifecycleTaskId || !target.lifecycleLeaseId || !target.lifecycleWorkerId)
|
|
217
393
|
return;
|
|
218
|
-
|
|
394
|
+
const retainedReason = sanitizeRetainedCode(reason);
|
|
395
|
+
registry.orchestrator.completeTask({
|
|
219
396
|
taskId: target.lifecycleTaskId,
|
|
220
397
|
leaseId: target.lifecycleLeaseId,
|
|
221
398
|
workerId: target.lifecycleWorkerId,
|
|
222
|
-
output:
|
|
223
|
-
reason,
|
|
399
|
+
output: toRetainedJsonValue({
|
|
400
|
+
reason: retainedReason,
|
|
224
401
|
...(extra ? { extra } : {}),
|
|
225
402
|
}),
|
|
226
403
|
nextContext: buildSessionContext(target, 'session.completed', {
|
|
227
|
-
reason,
|
|
404
|
+
reason: retainedReason,
|
|
228
405
|
...(extra ? { ...extra } : {}),
|
|
229
406
|
}),
|
|
230
407
|
nextContextLabel: 'session.completed',
|
|
231
408
|
});
|
|
409
|
+
secureAfterWrite(registry);
|
|
232
410
|
target.lifecycleFinalized = true;
|
|
233
411
|
}
|
|
234
412
|
export function failSessionLifecycle(target, error, extra) {
|
|
235
|
-
|
|
413
|
+
const registry = currentSessionLifecycleRegistry();
|
|
414
|
+
if (!registry?.orchestrator)
|
|
236
415
|
return;
|
|
237
|
-
const orchestrator = lifecycleRegistry.orchestrator;
|
|
238
416
|
if (target.lifecycleFinalized || !target.lifecycleTaskId || !target.lifecycleLeaseId || !target.lifecycleWorkerId)
|
|
239
417
|
return;
|
|
418
|
+
const retainedError = sanitizeRetainedError(error);
|
|
240
419
|
recordSessionSnapshot(target, 'session.failed', {
|
|
241
|
-
error,
|
|
420
|
+
error: retainedError,
|
|
242
421
|
...(extra ? { ...extra } : {}),
|
|
243
422
|
});
|
|
244
|
-
orchestrator.failTask({
|
|
423
|
+
registry.orchestrator.failTask({
|
|
245
424
|
taskId: target.lifecycleTaskId,
|
|
246
425
|
leaseId: target.lifecycleLeaseId,
|
|
247
426
|
workerId: target.lifecycleWorkerId,
|
|
248
|
-
error,
|
|
249
|
-
metadata: extra ?
|
|
427
|
+
error: retainedError,
|
|
428
|
+
metadata: extra ? toRetainedJsonValue(extra) : undefined,
|
|
250
429
|
});
|
|
430
|
+
secureAfterWrite(registry);
|
|
251
431
|
target.lifecycleFinalized = true;
|
|
252
432
|
}
|
|
253
433
|
export function shutdownSessionLifecycleRegistry() {
|
|
254
|
-
lifecycleRegistry
|
|
434
|
+
const registry = lifecycleRegistry;
|
|
435
|
+
lifecycleRegistry = null;
|
|
436
|
+
registry?.close();
|
|
255
437
|
}
|