@lark-apaas/fullstack-cli 1.1.59-alpha.20260722050835 → 1.1.59-alpha.20260722071909

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.
@@ -1,2155 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { spawnSync: nodeSpawnSync } = require('child_process');
6
-
7
- const DEFAULT_OUTBOX_ROOT = '/tmp/event/miaoda-preview-phase/v1';
8
- const MAX_EVENT_BYTES = 16 * 1024;
9
- const DEFAULT_MAX_RUN_BYTES = 2 * 1024 * 1024;
10
- const DEFAULT_MAX_ROOT_BYTES = 64 * 1024 * 1024;
11
- const DEFAULT_MAX_ROOT_DIRECTORIES = 128;
12
- const DEFAULT_MAX_ROOT_SCAN_DIRECTORIES = 256;
13
- const DEFAULT_MAX_RUN_SCAN_ENTRIES = 64;
14
- const DEFAULT_MAX_FALLBACK_FILES = 32;
15
- const DEFAULT_MAX_FALLBACK_BYTES = 2 * 1024 * 1024;
16
- const DEFAULT_MAX_FALLBACK_EVENTS = 256;
17
- const MAX_FALLBACK_RECORD_BYTES = MAX_EVENT_BYTES + 4 * 1024;
18
- const FALLBACK_TERMINAL_RESERVE_BYTES = 1024;
19
- const RUN_ID_PATTERN = /^prv_[A-Za-z0-9_-]{1,60}$/;
20
- const STATE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
21
- const PRODUCER_INSTANCE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
22
- const FALLBACK_SESSION_ID_PATTERN = /^pfs_[a-f0-9]{32}$/;
23
- const PROCESS_START_TICKS_PATTERN = /^\d+$/;
24
- const PRODUCER_EPOCH_PATTERN = /^ppe_[a-f0-9]{32}$/;
25
- const INSTANCE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/;
26
- const PRODUCER_REGISTER_URL = 'http://127.0.0.1:8091/shell/exec';
27
- const PRODUCER_REGISTER_ATTEMPTS = 2;
28
- const PRODUCER_REGISTER_CURL_TIMEOUT_SECONDS = '0.35';
29
- const PRODUCER_REGISTER_PROCESS_TIMEOUT_MS = 400;
30
- const PRODUCER_REGISTER_MAX_RESPONSE_BYTES = 64 * 1024;
31
- const TRANSIENT_ERRNO_CODES = new Set([
32
- 'EAGAIN',
33
- 'EINTR',
34
- 'EBUSY',
35
- 'EMFILE',
36
- 'ENFILE',
37
- 'ENOMEM',
38
- ]);
39
- const MAX_TRANSIENT_ATTEMPTS = 3;
40
-
41
- function producerRegistrationError(code, message, cause) {
42
- const error = new Error(message, cause == null ? undefined : { cause });
43
- error.code = code;
44
- return error;
45
- }
46
-
47
- function validateProducerRegistrationResponse(response, request) {
48
- const commonInvalid =
49
- response?.protocol !== 'preview-phase-outbox-v1' ||
50
- response.capability_status !== 'supported' ||
51
- response.generation !== request.generation ||
52
- response.producer_epoch !== request.producer_epoch ||
53
- response.producer_generation !== request.generation;
54
- const rejected = response?.registration_status === 'rejected';
55
- const rejectedInvalid =
56
- rejected &&
57
- (response.registration_reason !== 'stale_epoch' ||
58
- response.writer_active === true ||
59
- response.sealed !== true ||
60
- response.fallback_required !== true ||
61
- response.fallback_session_id !== request.fallback_session_id ||
62
- response.fallback_final_seq !== 1 ||
63
- response.producer_state !== 'rejected' ||
64
- request.rejected_event == null);
65
- const acceptedInvalid =
66
- !rejected &&
67
- ((response?.registration_status != null &&
68
- response.registration_status !== 'accepted') ||
69
- response.writer_active !== true ||
70
- response.producer_state !== 'active');
71
- if (commonInvalid || rejectedInvalid || acceptedInvalid) {
72
- throw producerRegistrationError(
73
- 'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN',
74
- 'invalid preview phase producer registration response'
75
- );
76
- }
77
- return response;
78
- }
79
-
80
- function registerPreviewPhaseProducerViaShellServer(
81
- request,
82
- spawnSync = nodeSpawnSync
83
- ) {
84
- const wireRequest = {
85
- phase_outbox: {
86
- protocol: 'preview-phase-outbox-v1',
87
- operation: 'register',
88
- run_id: request.run_id,
89
- generation: request.generation,
90
- expected_instance_id: request.expected_instance_id,
91
- producer_epoch: request.producer_epoch,
92
- writer_instance_id: request.writer_instance_id,
93
- pid: request.pid,
94
- process_start_ticks: request.process_start_ticks,
95
- ...(request.rejected_event == null
96
- ? {}
97
- : {
98
- producer_instance_id: request.producer_instance_id,
99
- fallback_session_id: request.fallback_session_id,
100
- rejected_event: request.rejected_event,
101
- }),
102
- },
103
- };
104
- const result = spawnSync(
105
- 'curl',
106
- [
107
- '--silent',
108
- '--show-error',
109
- '--connect-timeout',
110
- PRODUCER_REGISTER_CURL_TIMEOUT_SECONDS,
111
- '--max-time',
112
- PRODUCER_REGISTER_CURL_TIMEOUT_SECONDS,
113
- '--request',
114
- 'POST',
115
- '--header',
116
- 'Content-Type: application/json',
117
- '--data-binary',
118
- '@-',
119
- PRODUCER_REGISTER_URL,
120
- ],
121
- {
122
- input: Buffer.from(JSON.stringify(wireRequest), 'utf8'),
123
- encoding: 'utf8',
124
- timeout: PRODUCER_REGISTER_PROCESS_TIMEOUT_MS,
125
- maxBuffer: PRODUCER_REGISTER_MAX_RESPONSE_BYTES,
126
- windowsHide: true,
127
- }
128
- );
129
- if (result?.error) {
130
- const code =
131
- result.error.code === 'ETIMEDOUT'
132
- ? 'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN'
133
- : 'PREVIEW_PHASE_PRODUCER_REGISTRATION_UNAVAILABLE';
134
- throw producerRegistrationError(
135
- code,
136
- `preview phase producer registration transport failed: ${result.error.message}`,
137
- result.error
138
- );
139
- }
140
- if (result?.signal || result?.status === 28) {
141
- throw producerRegistrationError(
142
- 'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN',
143
- 'preview phase producer registration acknowledgement timed out'
144
- );
145
- }
146
- if (result?.status !== 0) {
147
- throw producerRegistrationError(
148
- 'PREVIEW_PHASE_PRODUCER_REGISTRATION_UNAVAILABLE',
149
- `preview phase producer registration transport exited ${String(result?.status)}`
150
- );
151
- }
152
- let envelope;
153
- try {
154
- envelope = JSON.parse(result.stdout || '');
155
- } catch (error) {
156
- throw producerRegistrationError(
157
- 'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN',
158
- 'preview phase producer registration returned invalid JSON',
159
- error
160
- );
161
- }
162
- if (envelope?.success !== true || envelope?.data?.success !== true) {
163
- const detail =
164
- envelope?.data?.error || envelope?.error || envelope?.message || 'rejected';
165
- const detailText = String(detail);
166
- let code = 'PREVIEW_PHASE_PRODUCER_REGISTRATION_REJECTED';
167
- if (detailText.includes('registration acknowledgement unknown')) {
168
- code = 'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN';
169
- } else if (detailText.includes('producer epoch already belongs')) {
170
- code = 'PREVIEW_PHASE_PRODUCER_EPOCH_CONFLICT';
171
- } else if (
172
- detailText.includes('publish preview phase canonical registration') ||
173
- detailText.includes('conflicts with canonical run')
174
- ) {
175
- code = 'PREVIEW_PHASE_RUN_CONFLICT';
176
- }
177
- throw producerRegistrationError(
178
- code,
179
- `preview phase producer registration rejected: ${detailText}`
180
- );
181
- }
182
- let response;
183
- try {
184
- response = JSON.parse(envelope.data.output || '');
185
- } catch (error) {
186
- throw producerRegistrationError(
187
- 'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN',
188
- 'preview phase producer registration acknowledgement was invalid',
189
- error
190
- );
191
- }
192
- return validateProducerRegistrationResponse(response, request);
193
- }
194
-
195
- function readLinuxProcessStartTicks(fileSystem, pid) {
196
- try {
197
- const stat = fileSystem.readFileSync(`/proc/${pid}/stat`, 'utf8');
198
- // comm (field 2) may contain spaces or parentheses, so split only after
199
- // its final closing parenthesis. The remaining first field is state (3),
200
- // therefore starttime (22) is at index 19.
201
- const commEnd = stat.lastIndexOf(')');
202
- if (commEnd < 0) return null;
203
- const fields = stat
204
- .slice(commEnd + 1)
205
- .trim()
206
- .split(/\s+/);
207
- const startTicks = fields[19];
208
- return PROCESS_START_TICKS_PATTERN.test(startTicks || '')
209
- ? startTicks
210
- : null;
211
- } catch {
212
- return null;
213
- }
214
- }
215
-
216
- function assertSafeDirectory(fileSystem, directoryPath) {
217
- if (!fileSystem.existsSync(directoryPath)) {
218
- fileSystem.mkdirSync(directoryPath, { recursive: true, mode: 0o700 });
219
- }
220
- const stat = fileSystem.lstatSync(directoryPath);
221
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
222
- throw new Error(`unsafe preview phase outbox path: ${directoryPath}`);
223
- }
224
- fileSystem.chmodSync(directoryPath, 0o700);
225
- }
226
-
227
- // Canonical generations are published only by the staging-directory rename.
228
- // Every later lookup must be existing-only: recreating a path that the
229
- // controller just quarantined would hide the durable generation and could
230
- // manufacture a second, unrelated canonical lifecycle.
231
- function assertExistingSafeDirectory(fileSystem, directoryPath) {
232
- const stat = fileSystem.lstatSync(directoryPath);
233
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
234
- throw new Error(`unsafe preview phase outbox path: ${directoryPath}`);
235
- }
236
- fileSystem.chmodSync(directoryPath, 0o700);
237
- }
238
-
239
- function createFallbackSessionID(randomUUID = require('crypto').randomUUID) {
240
- const sessionID = `pfs_${String(randomUUID())
241
- .replaceAll('-', '')
242
- .toLowerCase()}`;
243
- if (!FALLBACK_SESSION_ID_PATTERN.test(sessionID)) {
244
- throw new Error('invalid preview phase fallback session UUID');
245
- }
246
- return sessionID;
247
- }
248
-
249
- function createPreviewPhaseOutbox(options) {
250
- const fileSystem = options.fs || fs;
251
- const root = options.root || DEFAULT_OUTBOX_ROOT;
252
- const randomUUID = options.random_uuid || require('crypto').randomUUID;
253
- const now = options.now || Date.now;
254
- const writerInstanceID = options.writer_instance_id || randomUUID();
255
- const producerInstanceID = options.producer_instance_id || writerInstanceID;
256
- const fallbackSessionID =
257
- options.fallback_session_id || createFallbackSessionID();
258
- const producerEpoch = options.producer_epoch;
259
- const expectedInstanceID = options.expected_instance_id;
260
- const registerProducer =
261
- options.register_producer ||
262
- (request =>
263
- registerPreviewPhaseProducerViaShellServer(
264
- request,
265
- options.spawn_sync || nodeSpawnSync
266
- ));
267
- const getProcessStartTicks =
268
- options.get_process_start_ticks ||
269
- (pid => readLinuxProcessStartTicks(fileSystem, pid));
270
- const configuredProcessStartTicks = options.process_start_ticks;
271
- const currentProcessStartTicks =
272
- configuredProcessStartTicks == null
273
- ? getProcessStartTicks(process.pid)
274
- : configuredProcessStartTicks;
275
- const maxRunBytes = options.max_run_bytes ?? DEFAULT_MAX_RUN_BYTES;
276
- const maxRootBytes = options.max_root_bytes ?? DEFAULT_MAX_ROOT_BYTES;
277
- const maxRootDirectories =
278
- options.max_root_directories ?? DEFAULT_MAX_ROOT_DIRECTORIES;
279
- const maxRootScanDirectories =
280
- options.max_root_scan_directories ?? DEFAULT_MAX_ROOT_SCAN_DIRECTORIES;
281
- const maxRunScanEntries =
282
- options.max_run_scan_entries ?? DEFAULT_MAX_RUN_SCAN_ENTRIES;
283
- const maxFallbackFiles =
284
- options.max_fallback_files ??
285
- Math.min(DEFAULT_MAX_FALLBACK_FILES, maxRunScanEntries);
286
- const maxFallbackBytes =
287
- options.max_fallback_bytes ?? DEFAULT_MAX_FALLBACK_BYTES;
288
- const maxFallbackEvents =
289
- options.max_fallback_events ?? DEFAULT_MAX_FALLBACK_EVENTS;
290
- const maxFallbackRecordBytes =
291
- options.max_fallback_record_bytes ?? MAX_FALLBACK_RECORD_BYTES;
292
- if (!RUN_ID_PATTERN.test(options.run_id || '')) {
293
- throw new Error('invalid preview run_id for phase outbox');
294
- }
295
- if (!PRODUCER_EPOCH_PATTERN.test(producerEpoch || '')) {
296
- throw new Error('invalid preview phase producer_epoch');
297
- }
298
- if (!INSTANCE_ID_PATTERN.test(expectedInstanceID || '')) {
299
- throw new Error('invalid preview phase expected_instance_id');
300
- }
301
- if (typeof registerProducer !== 'function') {
302
- throw new Error('invalid preview phase producer registrar');
303
- }
304
- if (!STATE_ID_PATTERN.test(writerInstanceID)) {
305
- throw new Error('invalid preview phase writer_instance_id');
306
- }
307
- if (!STATE_ID_PATTERN.test(producerInstanceID)) {
308
- throw new Error('invalid preview phase producer_instance_id');
309
- }
310
- if (!FALLBACK_SESSION_ID_PATTERN.test(fallbackSessionID)) {
311
- throw new Error('invalid preview phase fallback_session_id');
312
- }
313
- if (!PROCESS_START_TICKS_PATTERN.test(currentProcessStartTicks || '')) {
314
- throw new Error('invalid preview phase process_start_ticks');
315
- }
316
- if (!Number.isSafeInteger(maxRunBytes) || maxRunBytes <= 0) {
317
- throw new Error('invalid preview phase max_run_bytes');
318
- }
319
- if (!Number.isSafeInteger(maxRootBytes) || maxRootBytes <= 0) {
320
- throw new Error('invalid preview phase max_root_bytes');
321
- }
322
- if (!Number.isSafeInteger(maxRootDirectories) || maxRootDirectories <= 0) {
323
- throw new Error('invalid preview phase max_root_directories');
324
- }
325
- if (
326
- !Number.isSafeInteger(maxRootScanDirectories) ||
327
- maxRootScanDirectories < maxRootDirectories
328
- ) {
329
- throw new Error('invalid preview phase max_root_scan_directories');
330
- }
331
- if (!Number.isSafeInteger(maxRunScanEntries) || maxRunScanEntries <= 0) {
332
- throw new Error('invalid preview phase max_run_scan_entries');
333
- }
334
- if (
335
- !Number.isSafeInteger(maxFallbackFiles) ||
336
- maxFallbackFiles <= 0 ||
337
- maxFallbackFiles > maxRunScanEntries
338
- ) {
339
- throw new Error('invalid preview phase max_fallback_files');
340
- }
341
- if (!Number.isSafeInteger(maxFallbackBytes) || maxFallbackBytes <= 0) {
342
- throw new Error('invalid preview phase max_fallback_bytes');
343
- }
344
- if (!Number.isSafeInteger(maxFallbackEvents) || maxFallbackEvents <= 0) {
345
- throw new Error('invalid preview phase max_fallback_events');
346
- }
347
- if (
348
- !Number.isSafeInteger(maxFallbackRecordBytes) ||
349
- maxFallbackRecordBytes <= 0
350
- ) {
351
- throw new Error('invalid preview phase max_fallback_record_bytes');
352
- }
353
- const canonicalRunDir = path.join(root, options.run_id);
354
- const stagingName = `.staging-${process.pid}-${writerInstanceID}`;
355
- const stagingRunDir = path.join(root, stagingName);
356
- let runDir = canonicalRunDir;
357
- let eventsPath = path.join(runDir, 'events.jsonl');
358
- let metaPath = path.join(runDir, 'meta.json');
359
- let writerLockPath = path.join(runDir, 'writer.lock');
360
- let sealPath = path.join(runDir, 'sealed.json');
361
- let degradedPath = path.join(runDir, 'degraded.json');
362
- let fallbackIntentPath = path.join(runDir, 'fallback.intent.json');
363
- let quiesceRequestPath = path.join(runDir, 'quiesce.request.json');
364
- let unboundFallbackPath = path.join(runDir, 'producer.unbound.json');
365
- const fallbackSpoolPath = path.join(
366
- canonicalRunDir,
367
- `fallback-${fallbackSessionID}.jsonl`
368
- );
369
- const producerEpochMarkerPath = path.join(
370
- root,
371
- `producer-epoch-${producerEpoch}.json`
372
- );
373
- let recovered = false;
374
- let metadata = null;
375
- let writerLockOwned = false;
376
- let writerLockDirectory = null;
377
- let eventsFD = null;
378
- let committedBytes = null;
379
- let degraded = false;
380
- let fallbackRequired = false;
381
- let sealRequested = false;
382
- let closed = false;
383
- let closeResult = false;
384
- let producerMarkerActive = false;
385
- let producerMarkerTerminal = false;
386
- let producerRegistrationDurable = false;
387
- let producerRegistrationAttemptCount = 0;
388
- let producerRegistrationLastError = null;
389
- let producerStartedAtMs = null;
390
- let producerTerminalAtMs = null;
391
- let canonicalPublicationDurable = false;
392
- let everOwnedCanonicalWriter = false;
393
- let fallbackFD = null;
394
- let fallbackBytes = 0;
395
- let fallbackSeq = 0;
396
- let fallbackStarted = false;
397
- let fallbackSealed = false;
398
- let fallbackFailed = false;
399
- let fallbackCreatedAt = null;
400
- function selectRunDirectory(directoryPath) {
401
- if (runDir !== directoryPath) {
402
- if (eventsFD != null) {
403
- throw new Error(
404
- 'cannot switch preview phase directory with an open events fd'
405
- );
406
- }
407
- metadata = null;
408
- recovered = false;
409
- committedBytes = null;
410
- }
411
- runDir = directoryPath;
412
- eventsPath = path.join(runDir, 'events.jsonl');
413
- metaPath = path.join(runDir, 'meta.json');
414
- writerLockPath = path.join(runDir, 'writer.lock');
415
- sealPath = path.join(runDir, 'sealed.json');
416
- degradedPath = path.join(runDir, 'degraded.json');
417
- fallbackIntentPath = path.join(runDir, 'fallback.intent.json');
418
- quiesceRequestPath = path.join(runDir, 'quiesce.request.json');
419
- unboundFallbackPath = path.join(runDir, 'producer.unbound.json');
420
- }
421
-
422
- function ownsSelectedWriterLock() {
423
- return writerLockOwned && writerLockDirectory === runDir;
424
- }
425
-
426
- function isTransientFilesystemError(error) {
427
- return TRANSIENT_ERRNO_CODES.has(error?.code);
428
- }
429
-
430
- function rootQuotaError(message) {
431
- const error = new Error(message);
432
- error.code = 'PREVIEW_PHASE_ROOT_QUOTA_EXCEEDED';
433
- return error;
434
- }
435
-
436
- function readDirectoryEntriesBounded(directoryPath, limit, quotaMessage) {
437
- const entries = [];
438
- const directory = fileSystem.opendirSync(directoryPath);
439
- try {
440
- while (entries.length <= limit) {
441
- const entry = directory.readSync();
442
- if (!entry) break;
443
- entries.push(entry);
444
- if (entries.length > limit) {
445
- throw rootQuotaError(quotaMessage);
446
- }
447
- }
448
- } finally {
449
- directory.closeSync();
450
- }
451
- return entries;
452
- }
453
-
454
- function inspectRootUsage() {
455
- const entries = readDirectoryEntriesBounded(
456
- root,
457
- maxRootScanDirectories,
458
- `preview phase root scan exceeds ${maxRootScanDirectories} entries`
459
- );
460
- let directories = 0;
461
- let bytes = 0;
462
- const disappearingEntry = error =>
463
- error?.code === 'ENOENT' || error?.code === 'ENOTDIR';
464
- for (const entry of entries) {
465
- const entryPath = path.join(root, entry.name);
466
- let stat;
467
- try {
468
- stat = fileSystem.lstatSync(entryPath);
469
- } catch (error) {
470
- // The Go consumer atomically renames and removes acknowledged runs.
471
- // A root snapshot can therefore contain an entry that is gone by the
472
- // following lstat; disappearance is successful GC, not degradation.
473
- if (disappearingEntry(error)) continue;
474
- throw error;
475
- }
476
- if (stat.isSymbolicLink()) {
477
- throw new Error(`unsafe preview phase outbox path: ${entryPath}`);
478
- }
479
- if (stat.isFile()) {
480
- bytes += stat.size;
481
- continue;
482
- }
483
- if (!stat.isDirectory()) {
484
- throw new Error(`unsafe preview phase outbox path: ${entryPath}`);
485
- }
486
- directories += 1;
487
- let runEntries;
488
- try {
489
- runEntries = readDirectoryEntriesBounded(
490
- entryPath,
491
- maxRunScanEntries,
492
- `preview phase run scan exceeds ${maxRunScanEntries} entries: ${entryPath}`
493
- );
494
- } catch (error) {
495
- if (disappearingEntry(error)) continue;
496
- throw error;
497
- }
498
- for (const runEntry of runEntries) {
499
- const statePath = path.join(entryPath, runEntry.name);
500
- let stateStat;
501
- try {
502
- stateStat = fileSystem.lstatSync(statePath);
503
- } catch (error) {
504
- if (disappearingEntry(error)) continue;
505
- throw error;
506
- }
507
- if (stateStat.isSymbolicLink() || !stateStat.isFile()) {
508
- throw new Error(`unsafe preview phase outbox path: ${statePath}`);
509
- }
510
- bytes += stateStat.size;
511
- }
512
- }
513
- return { bytes, directories };
514
- }
515
-
516
- function assertRootDirectoryQuota() {
517
- const usage = inspectRootUsage();
518
- if (
519
- !fileSystem.existsSync(runDir) &&
520
- usage.directories >= maxRootDirectories
521
- ) {
522
- throw rootQuotaError(
523
- `preview phase root directory quota reached ${maxRootDirectories}`
524
- );
525
- }
526
- }
527
-
528
- function assertRootByteQuota(recordBytes) {
529
- const usage = inspectRootUsage();
530
- if (usage.bytes + recordBytes > maxRootBytes) {
531
- throw rootQuotaError(
532
- `preview phase root quota would exceed ${maxRootBytes} bytes`
533
- );
534
- }
535
- }
536
-
537
- function readRegularJSON(filePath, description) {
538
- const stat = fileSystem.lstatSync(filePath);
539
- if (!stat.isFile() || stat.isSymbolicLink()) {
540
- throw new Error(`unsafe preview phase outbox path: ${filePath}`);
541
- }
542
- const value = JSON.parse(fileSystem.readFileSync(filePath, 'utf8'));
543
- if (!value || typeof value !== 'object') {
544
- throw new Error(`invalid preview phase ${description}: ${filePath}`);
545
- }
546
- return value;
547
- }
548
-
549
- function syncDirectory(directoryPath) {
550
- const flags =
551
- fs.constants.O_RDONLY |
552
- (fs.constants.O_DIRECTORY || 0) |
553
- (fs.constants.O_NOFOLLOW || 0);
554
- const fd = fileSystem.openSync(directoryPath, flags);
555
- try {
556
- fileSystem.fsyncSync(fd);
557
- } finally {
558
- fileSystem.closeSync(fd);
559
- }
560
- }
561
-
562
- function syncRunDirectory() {
563
- syncDirectory(runDir);
564
- }
565
-
566
- function syncRootDirectory() {
567
- const flags =
568
- fs.constants.O_RDONLY |
569
- (fs.constants.O_DIRECTORY || 0) |
570
- (fs.constants.O_NOFOLLOW || 0);
571
- const fd = fileSystem.openSync(root, flags);
572
- try {
573
- fileSystem.fsyncSync(fd);
574
- } finally {
575
- fileSystem.closeSync(fd);
576
- }
577
- }
578
-
579
- function writeAll(fd, payload) {
580
- let offset = 0;
581
- while (offset < payload.length) {
582
- const written = fileSystem.writeSync(
583
- fd,
584
- payload,
585
- offset,
586
- payload.length - offset
587
- );
588
- if (!Number.isInteger(written) || written <= 0) {
589
- throw new Error('preview phase state write made no progress');
590
- }
591
- offset += written;
592
- }
593
- }
594
-
595
- function writeDurableTempFile(tempPath, payload) {
596
- let fd = null;
597
- try {
598
- const flags =
599
- fs.constants.O_CREAT |
600
- fs.constants.O_EXCL |
601
- fs.constants.O_WRONLY |
602
- (fs.constants.O_NOFOLLOW || 0);
603
- fd = fileSystem.openSync(tempPath, flags, 0o600);
604
- writeAll(fd, payload);
605
- fileSystem.fsyncSync(fd);
606
- } finally {
607
- if (fd != null) {
608
- fileSystem.closeSync(fd);
609
- }
610
- }
611
- }
612
-
613
- function publishImmutableState(filePath, label, value) {
614
- const tempPath = path.join(
615
- runDir,
616
- `.${label}.${process.pid}.${writerInstanceID}.tmp`
617
- );
618
- let fd = null;
619
- try {
620
- const flags =
621
- fs.constants.O_CREAT |
622
- fs.constants.O_EXCL |
623
- fs.constants.O_WRONLY |
624
- (fs.constants.O_NOFOLLOW || 0);
625
- fd = fileSystem.openSync(tempPath, flags, 0o600);
626
- writeAll(fd, Buffer.from(`${JSON.stringify(value)}\n`, 'utf8'));
627
- fileSystem.fsyncSync(fd);
628
- fileSystem.closeSync(fd);
629
- fd = null;
630
- fileSystem.linkSync(tempPath, filePath);
631
- syncRunDirectory();
632
- } finally {
633
- if (fd != null) {
634
- try {
635
- fileSystem.closeSync(fd);
636
- } catch {
637
- // Best-effort fd cleanup.
638
- }
639
- }
640
- try {
641
- fileSystem.rmSync(tempPath, { force: true });
642
- } catch {
643
- // A stale temp file is harmless and never consumed by the reader.
644
- }
645
- }
646
- }
647
-
648
- function producerMarkerConflict() {
649
- const error = new Error(
650
- `preview phase producer epoch already belongs to another writer: ${producerEpoch}`
651
- );
652
- error.code = 'PREVIEW_PHASE_PRODUCER_EPOCH_CONFLICT';
653
- return error;
654
- }
655
-
656
- function validateProducerMarker(marker) {
657
- const terminal = marker?.state === 'terminal';
658
- if (
659
- marker?.protocol !== 'preview-phase-producer-epoch-v1' ||
660
- marker.schema_version !== 1 ||
661
- marker.epoch !== producerEpoch ||
662
- !RUN_ID_PATTERN.test(marker.run_id || '') ||
663
- !STATE_ID_PATTERN.test(marker.generation || '') ||
664
- !STATE_ID_PATTERN.test(marker.writer_instance_id || '') ||
665
- !Number.isSafeInteger(marker.pid) ||
666
- marker.pid <= 0 ||
667
- !PROCESS_START_TICKS_PATTERN.test(marker.process_start_ticks || '') ||
668
- !Number.isFinite(marker.started_at_ms) ||
669
- marker.started_at_ms <= 0 ||
670
- (marker.state !== 'active' && !terminal) ||
671
- (terminal &&
672
- (!Number.isFinite(marker.terminal_at_ms) ||
673
- marker.terminal_at_ms < marker.started_at_ms)) ||
674
- (!terminal && marker.terminal_at_ms !== undefined)
675
- ) {
676
- throw new Error(
677
- `invalid preview phase producer epoch marker: ${producerEpochMarkerPath}`
678
- );
679
- }
680
- return marker;
681
- }
682
-
683
- function markerBelongsToThisWriter(marker) {
684
- return (
685
- marker.epoch === producerEpoch &&
686
- marker.run_id === options.run_id &&
687
- marker.generation === metadata.generation &&
688
- marker.writer_instance_id === writerInstanceID &&
689
- marker.pid === process.pid &&
690
- (marker.process_start_ticks || '') === (currentProcessStartTicks || '')
691
- );
692
- }
693
-
694
- function readAndValidateProducerMarker() {
695
- return validateProducerMarker(
696
- readRegularJSON(producerEpochMarkerPath, 'producer epoch marker')
697
- );
698
- }
699
-
700
- function buildProducerMarker(state) {
701
- return {
702
- protocol: 'preview-phase-producer-epoch-v1',
703
- schema_version: 1,
704
- epoch: producerEpoch,
705
- run_id: options.run_id,
706
- generation: metadata.generation,
707
- writer_instance_id: writerInstanceID,
708
- pid: process.pid,
709
- process_start_ticks: currentProcessStartTicks,
710
- started_at_ms: producerStartedAtMs,
711
- state,
712
- ...(state === 'terminal' ? { terminal_at_ms: producerTerminalAtMs } : {}),
713
- };
714
- }
715
-
716
- function publishActiveProducerMarker() {
717
- if (producerMarkerActive) return;
718
- if (
719
- !producerRegistrationDurable ||
720
- !metadata ||
721
- !ownsSelectedWriterLock()
722
- ) {
723
- throw new Error(
724
- 'preview phase producer marker requires authoritative registration'
725
- );
726
- }
727
- const existing = readAndValidateProducerMarker();
728
- if (!markerBelongsToThisWriter(existing) || existing.state !== 'active') {
729
- throw producerMarkerConflict();
730
- }
731
- producerStartedAtMs = existing.started_at_ms;
732
- producerMarkerActive = true;
733
- }
734
-
735
- function publishTerminalProducerMarker() {
736
- if (producerMarkerTerminal) return;
737
- const existing = readAndValidateProducerMarker();
738
- if (!markerBelongsToThisWriter(existing)) {
739
- throw producerMarkerConflict();
740
- }
741
- producerStartedAtMs = existing.started_at_ms;
742
- if (existing.state === 'terminal') {
743
- producerTerminalAtMs = existing.terminal_at_ms;
744
- producerMarkerActive = true;
745
- producerMarkerTerminal = true;
746
- return;
747
- }
748
- producerTerminalAtMs =
749
- producerTerminalAtMs ?? Math.max(now(), producerStartedAtMs);
750
- const marker = buildProducerMarker('terminal');
751
- const tempPath = path.join(
752
- root,
753
- `.producer-terminal.${producerEpoch}.${process.pid}.${writerInstanceID}.tmp`
754
- );
755
- let fd = null;
756
- try {
757
- const flags =
758
- fs.constants.O_CREAT |
759
- fs.constants.O_EXCL |
760
- fs.constants.O_WRONLY |
761
- (fs.constants.O_NOFOLLOW || 0);
762
- fd = fileSystem.openSync(tempPath, flags, 0o600);
763
- writeAll(fd, Buffer.from(`${JSON.stringify(marker)}\n`, 'utf8'));
764
- fileSystem.fsyncSync(fd);
765
- fileSystem.closeSync(fd);
766
- fd = null;
767
- fileSystem.renameSync(tempPath, producerEpochMarkerPath);
768
- syncRootDirectory();
769
- producerMarkerActive = true;
770
- producerMarkerTerminal = true;
771
- } finally {
772
- if (fd != null) {
773
- try {
774
- fileSystem.closeSync(fd);
775
- } catch {
776
- // Best-effort fd cleanup.
777
- }
778
- }
779
- let tempRemoved = false;
780
- try {
781
- fileSystem.rmSync(tempPath, { force: true });
782
- tempRemoved = true;
783
- } catch {
784
- // A stale temp file is never accepted as lifecycle proof.
785
- }
786
- if (tempRemoved) syncRootDirectory();
787
- }
788
- }
789
-
790
- function readAndValidateMetadataAt(directoryPath) {
791
- const candidateMetaPath = path.join(directoryPath, 'meta.json');
792
- const stat = fileSystem.lstatSync(candidateMetaPath);
793
- if (!stat.isFile() || stat.isSymbolicLink()) {
794
- throw new Error(`unsafe preview phase outbox path: ${candidateMetaPath}`);
795
- }
796
- fileSystem.chmodSync(candidateMetaPath, 0o600);
797
- const metadata = JSON.parse(
798
- fileSystem.readFileSync(candidateMetaPath, 'utf8')
799
- );
800
- if (
801
- metadata.protocol !== 'preview-phase-outbox-v1' ||
802
- metadata.schema_version !== 1 ||
803
- metadata.producer_epoch !== producerEpoch ||
804
- metadata.run_id !== options.run_id ||
805
- !/^[A-Za-z0-9_-]{1,128}$/.test(metadata.generation || '') ||
806
- !Number.isFinite(metadata.created_at_ms) ||
807
- metadata.created_at_ms <= 0 ||
808
- (metadata.quiesce_supported !== undefined &&
809
- metadata.quiesce_supported !== true)
810
- ) {
811
- throw new Error(
812
- `invalid preview phase outbox metadata: ${candidateMetaPath}`
813
- );
814
- }
815
- return metadata;
816
- }
817
-
818
- function readAndValidateMetadata() {
819
- return readAndValidateMetadataAt(runDir);
820
- }
821
-
822
- function ensureMetadata() {
823
- if (metadata) return;
824
- if (fileSystem.existsSync(metaPath)) {
825
- metadata = readAndValidateMetadata();
826
- return;
827
- }
828
- const candidate = {
829
- protocol: 'preview-phase-outbox-v1',
830
- schema_version: 1,
831
- producer_epoch: producerEpoch,
832
- generation: randomUUID(),
833
- run_id: options.run_id,
834
- created_at_ms: now(),
835
- quiesce_supported: true,
836
- };
837
- const tempPath = path.join(
838
- runDir,
839
- `.meta.${process.pid}.${candidate.generation}.tmp`
840
- );
841
- try {
842
- writeDurableTempFile(
843
- tempPath,
844
- Buffer.from(`${JSON.stringify(candidate)}\n`, 'utf8')
845
- );
846
- try {
847
- fileSystem.linkSync(tempPath, metaPath);
848
- syncRunDirectory();
849
- } catch (error) {
850
- if (error?.code !== 'EEXIST') throw error;
851
- }
852
- } finally {
853
- let removed = false;
854
- try {
855
- fileSystem.rmSync(tempPath, { force: true });
856
- removed = true;
857
- } catch {
858
- // A stale temp file is harmless and never consumed by the reader.
859
- }
860
- if (removed) syncRunDirectory();
861
- }
862
- metadata = readAndValidateMetadata();
863
- }
864
-
865
- function validateFallbackIntent(intent, canonicalMetadata) {
866
- if (
867
- intent.protocol !== 'preview-phase-fallback-intent-v1' ||
868
- intent.schema_version !== 1 ||
869
- intent.producer_epoch !== producerEpoch ||
870
- intent.run_id !== options.run_id ||
871
- intent.generation !== canonicalMetadata.generation ||
872
- !Number.isFinite(intent.created_at_ms) ||
873
- intent.created_at_ms <= 0 ||
874
- !STATE_ID_PATTERN.test(intent.reason || '')
875
- ) {
876
- throw new Error(
877
- `invalid preview phase fallback intent: ${path.join(
878
- canonicalRunDir,
879
- 'fallback.intent.json'
880
- )}`
881
- );
882
- }
883
- return intent;
884
- }
885
-
886
- function fallbackSpoolError(code, message, cause) {
887
- const error = new Error(message, cause == null ? undefined : { cause });
888
- error.code = code;
889
- return error;
890
- }
891
-
892
- function encodeFallbackRecord(record, description) {
893
- const payload = JSON.stringify(record);
894
- if (typeof payload !== 'string') {
895
- throw fallbackSpoolError(
896
- 'PREVIEW_PHASE_FALLBACK_RECORD_INVALID',
897
- `invalid preview phase fallback ${description}`
898
- );
899
- }
900
- const encoded = Buffer.from(`${payload}\n`, 'utf8');
901
- if (encoded.length > maxFallbackRecordBytes) {
902
- throw fallbackSpoolError(
903
- 'PREVIEW_PHASE_FALLBACK_RECORD_TOO_LARGE',
904
- `preview phase fallback ${description} exceeds ${maxFallbackRecordBytes} bytes`
905
- );
906
- }
907
- return encoded;
908
- }
909
-
910
- function fallbackTimestamp() {
911
- const timestamp = now();
912
- if (!Number.isFinite(timestamp) || timestamp <= 0) {
913
- throw fallbackSpoolError(
914
- 'PREVIEW_PHASE_FALLBACK_TIMESTAMP_INVALID',
915
- 'invalid preview phase fallback timestamp'
916
- );
917
- }
918
- return timestamp;
919
- }
920
-
921
- function closeFallbackFD() {
922
- if (fallbackFD == null) return;
923
- try {
924
- fileSystem.closeSync(fallbackFD);
925
- } catch {
926
- // The durable boundary is fdatasync, not close. Never reuse this fd.
927
- }
928
- fallbackFD = null;
929
- }
930
-
931
- function ensureFallbackSpool(initialEventBytes) {
932
- if (fallbackFD != null) return;
933
- if (fallbackSealed) {
934
- throw fallbackSpoolError(
935
- 'PREVIEW_PHASE_FALLBACK_SEALED',
936
- 'preview phase fallback spool is sealed'
937
- );
938
- }
939
- if (fallbackFailed) {
940
- throw fallbackSpoolError(
941
- 'PREVIEW_PHASE_FALLBACK_DEGRADED',
942
- 'preview phase fallback spool is degraded'
943
- );
944
- }
945
-
946
- assertSafeDirectory(fileSystem, root);
947
- assertExistingSafeDirectory(fileSystem, canonicalRunDir);
948
- const parentMetadata = readAndValidateMetadataAt(canonicalRunDir);
949
- const entries = readDirectoryEntriesBounded(
950
- canonicalRunDir,
951
- maxRunScanEntries,
952
- `preview phase run scan exceeds ${maxRunScanEntries} entries: ${canonicalRunDir}`
953
- );
954
- const fallbackFiles = entries.filter(entry =>
955
- /^fallback-pfs_[a-f0-9]{32}\.jsonl$/.test(entry.name)
956
- );
957
- if (fallbackFiles.length >= maxFallbackFiles) {
958
- throw fallbackSpoolError(
959
- 'PREVIEW_PHASE_FALLBACK_FILE_LIMIT',
960
- `preview phase fallback file limit reached ${maxFallbackFiles}`
961
- );
962
- }
963
- if (fileSystem.existsSync(fallbackSpoolPath)) {
964
- throw fallbackSpoolError(
965
- 'PREVIEW_PHASE_FALLBACK_SESSION_CONFLICT',
966
- `preview phase fallback session already exists: ${fallbackSessionID}`
967
- );
968
- }
969
-
970
- const createdAt = fallbackTimestamp();
971
- const metaRecord = encodeFallbackRecord(
972
- {
973
- protocol: 'preview-phase-fallback-spool-v1',
974
- schema_version: 1,
975
- record_type: 'meta',
976
- session_id: fallbackSessionID,
977
- producer_epoch: producerEpoch,
978
- run_id: options.run_id,
979
- parent_generation: parentMetadata.generation,
980
- producer_instance_id: producerInstanceID,
981
- pid: process.pid,
982
- process_start_ticks: currentProcessStartTicks,
983
- // Protocol timestamps use Unix epoch milliseconds. The compact field
984
- // name is retained because this is a versioned wire contract.
985
- created_at: createdAt,
986
- },
987
- 'meta record'
988
- );
989
- if (
990
- metaRecord.length + initialEventBytes + FALLBACK_TERMINAL_RESERVE_BYTES >
991
- maxFallbackBytes
992
- ) {
993
- throw fallbackSpoolError(
994
- 'PREVIEW_PHASE_FALLBACK_FILE_QUOTA_EXCEEDED',
995
- `preview phase fallback file quota would exceed ${maxFallbackBytes} bytes`
996
- );
997
- }
998
- assertRootByteQuota(
999
- metaRecord.length + initialEventBytes + FALLBACK_TERMINAL_RESERVE_BYTES
1000
- );
1001
-
1002
- const tempPath = path.join(
1003
- canonicalRunDir,
1004
- `.fallback-${fallbackSessionID}.${process.pid}.${writerInstanceID}.tmp`
1005
- );
1006
- const directoryFlags =
1007
- fs.constants.O_RDONLY |
1008
- (fs.constants.O_DIRECTORY || 0) |
1009
- (fs.constants.O_NOFOLLOW || 0);
1010
- let directoryFD = null;
1011
- let fd = null;
1012
- let published = false;
1013
- try {
1014
- directoryFD = fileSystem.openSync(canonicalRunDir, directoryFlags);
1015
- const fileFlags =
1016
- fs.constants.O_CREAT |
1017
- fs.constants.O_EXCL |
1018
- fs.constants.O_WRONLY |
1019
- fs.constants.O_APPEND |
1020
- (fs.constants.O_NOFOLLOW || 0);
1021
- fd = fileSystem.openSync(tempPath, fileFlags, 0o600);
1022
- fileSystem.fchmodSync(fd, 0o600);
1023
- writeAll(fd, metaRecord);
1024
- fileSystem.fsyncSync(fd);
1025
- try {
1026
- fileSystem.linkSync(tempPath, fallbackSpoolPath);
1027
- } catch (error) {
1028
- if (error?.code === 'EEXIST') {
1029
- throw fallbackSpoolError(
1030
- 'PREVIEW_PHASE_FALLBACK_SESSION_CONFLICT',
1031
- `preview phase fallback session already exists: ${fallbackSessionID}`,
1032
- error
1033
- );
1034
- }
1035
- throw error;
1036
- }
1037
- // Sync the already-open directory, so quarantine rename after link does
1038
- // not invalidate the durability proof for the published file name.
1039
- fileSystem.fsyncSync(directoryFD);
1040
- published = true;
1041
- try {
1042
- fileSystem.rmSync(tempPath);
1043
- fileSystem.fsyncSync(directoryFD);
1044
- } catch {
1045
- // A quarantine rename can make the old path unreachable. The hidden
1046
- // hard-link alias is harmless; the published session remains bounded.
1047
- }
1048
- fallbackFD = fd;
1049
- fd = null;
1050
- fallbackBytes = metaRecord.length;
1051
- fallbackStarted = true;
1052
- fallbackCreatedAt = createdAt;
1053
- } catch (error) {
1054
- fallbackFailed = true;
1055
- throw error;
1056
- } finally {
1057
- if (fd != null) {
1058
- try {
1059
- fileSystem.closeSync(fd);
1060
- } catch {
1061
- // Best-effort fd cleanup.
1062
- }
1063
- }
1064
- if (!published) {
1065
- try {
1066
- fileSystem.rmSync(tempPath, { force: true });
1067
- if (directoryFD != null) fileSystem.fsyncSync(directoryFD);
1068
- } catch {
1069
- // Best-effort temp cleanup after a failed publication.
1070
- }
1071
- }
1072
- if (directoryFD != null) {
1073
- try {
1074
- fileSystem.closeSync(directoryFD);
1075
- } catch {
1076
- // Best-effort directory-fd cleanup.
1077
- }
1078
- }
1079
- }
1080
- }
1081
-
1082
- function appendFallback(event) {
1083
- if (fallbackSealed) {
1084
- throw fallbackSpoolError(
1085
- 'PREVIEW_PHASE_FALLBACK_SEALED',
1086
- 'preview phase fallback spool is sealed'
1087
- );
1088
- }
1089
- if (fallbackFailed) {
1090
- throw fallbackSpoolError(
1091
- 'PREVIEW_PHASE_FALLBACK_DEGRADED',
1092
- 'preview phase fallback spool is degraded'
1093
- );
1094
- }
1095
- if (!event || typeof event !== 'object' || Array.isArray(event)) {
1096
- throw fallbackSpoolError(
1097
- 'PREVIEW_PHASE_FALLBACK_RECORD_INVALID',
1098
- 'invalid preview phase fallback event'
1099
- );
1100
- }
1101
- if (fallbackSeq >= maxFallbackEvents) {
1102
- throw fallbackSpoolError(
1103
- 'PREVIEW_PHASE_FALLBACK_EVENT_LIMIT',
1104
- `preview phase fallback event limit reached ${maxFallbackEvents}`
1105
- );
1106
- }
1107
- const nextSeq = fallbackSeq + 1;
1108
- const fallbackEvent = {
1109
- ...event,
1110
- transport: 'fallback_spool',
1111
- fallback_session_id: fallbackSessionID,
1112
- fallback_seq: nextSeq,
1113
- };
1114
- const eventPayload = JSON.stringify(fallbackEvent);
1115
- if (
1116
- typeof eventPayload !== 'string' ||
1117
- Buffer.byteLength(eventPayload, 'utf8') > MAX_EVENT_BYTES
1118
- ) {
1119
- throw fallbackSpoolError(
1120
- 'PREVIEW_PHASE_EVENT_TOO_LARGE',
1121
- `preview phase event exceeds ${MAX_EVENT_BYTES} bytes`
1122
- );
1123
- }
1124
- const record = encodeFallbackRecord(
1125
- {
1126
- protocol: 'preview-phase-fallback-spool-v1',
1127
- schema_version: 1,
1128
- record_type: 'event',
1129
- session_id: fallbackSessionID,
1130
- seq: nextSeq,
1131
- event: fallbackEvent,
1132
- },
1133
- 'event record'
1134
- );
1135
-
1136
- ensureFallbackSpool(record.length);
1137
- if (
1138
- fallbackBytes + record.length + FALLBACK_TERMINAL_RESERVE_BYTES >
1139
- maxFallbackBytes
1140
- ) {
1141
- throw fallbackSpoolError(
1142
- 'PREVIEW_PHASE_FALLBACK_FILE_QUOTA_EXCEEDED',
1143
- `preview phase fallback file quota would exceed ${maxFallbackBytes} bytes`
1144
- );
1145
- }
1146
- try {
1147
- writeAll(fallbackFD, record);
1148
- fileSystem.fdatasyncSync(fallbackFD);
1149
- fallbackBytes += record.length;
1150
- fallbackSeq = nextSeq;
1151
- fallbackRequired = true;
1152
- return true;
1153
- } catch (error) {
1154
- fallbackFailed = true;
1155
- closeFallbackFD();
1156
- throw error;
1157
- }
1158
- }
1159
-
1160
- function sealFallback() {
1161
- if (!fallbackStarted) return !fallbackFailed;
1162
- if (fallbackSealed) return true;
1163
- if (fallbackFailed || fallbackFD == null) return false;
1164
- const terminal = encodeFallbackRecord(
1165
- {
1166
- protocol: 'preview-phase-fallback-spool-v1',
1167
- schema_version: 1,
1168
- record_type: 'terminal',
1169
- session_id: fallbackSessionID,
1170
- final_cursor: fallbackBytes,
1171
- final_seq: fallbackSeq,
1172
- // Unix epoch milliseconds; never precedes this session's meta record.
1173
- sealed_at: Math.max(fallbackTimestamp(), fallbackCreatedAt),
1174
- recovered: false,
1175
- },
1176
- 'terminal record'
1177
- );
1178
- if (fallbackBytes + terminal.length > maxFallbackBytes) {
1179
- fallbackFailed = true;
1180
- closeFallbackFD();
1181
- return false;
1182
- }
1183
- try {
1184
- writeAll(fallbackFD, terminal);
1185
- fileSystem.fdatasyncSync(fallbackFD);
1186
- fallbackBytes += terminal.length;
1187
- fallbackSealed = true;
1188
- closeFallbackFD();
1189
- return true;
1190
- } catch {
1191
- fallbackFailed = true;
1192
- closeFallbackFD();
1193
- return false;
1194
- }
1195
- }
1196
-
1197
- function validateProcessIdentityState(state, description) {
1198
- if (
1199
- !Number.isSafeInteger(state.pid) ||
1200
- state.pid <= 0 ||
1201
- (state.process_start_ticks !== undefined &&
1202
- !PROCESS_START_TICKS_PATTERN.test(state.process_start_ticks))
1203
- ) {
1204
- throw new Error(`invalid preview phase ${description}`);
1205
- }
1206
- }
1207
-
1208
- function validateWriterLock(lock) {
1209
- assertStateGeneration(lock, 'writer lock');
1210
- validateProcessIdentityState(lock, 'writer lock');
1211
- if (
1212
- lock.protocol !== 'preview-phase-writer-v1' ||
1213
- lock.schema_version !== 1 ||
1214
- !STATE_ID_PATTERN.test(lock.writer_instance_id || '') ||
1215
- !Number.isFinite(lock.created_at_ms) ||
1216
- lock.created_at_ms <= 0
1217
- ) {
1218
- throw new Error(`invalid preview phase writer lock: ${writerLockPath}`);
1219
- }
1220
- return lock;
1221
- }
1222
-
1223
- function writerBusyError() {
1224
- const busy = new Error(
1225
- `preview phase generation already has an active writer: ${options.run_id}`
1226
- );
1227
- busy.code = 'PREVIEW_PHASE_WRITER_BUSY';
1228
- return busy;
1229
- }
1230
-
1231
- function runConflictError(cause) {
1232
- const conflict = new Error(
1233
- `preview phase canonical run was published by another writer: ${options.run_id}`,
1234
- { cause }
1235
- );
1236
- conflict.code = 'PREVIEW_PHASE_RUN_CONFLICT';
1237
- return conflict;
1238
- }
1239
-
1240
- function acquireWriterLock() {
1241
- if (ownsSelectedWriterLock()) return;
1242
- if (writerLockOwned) {
1243
- throw new Error(
1244
- 'preview phase writer ownership belongs to another directory'
1245
- );
1246
- }
1247
- const lock = {
1248
- protocol: 'preview-phase-writer-v1',
1249
- schema_version: 1,
1250
- run_id: options.run_id,
1251
- generation: metadata.generation,
1252
- writer_instance_id: writerInstanceID,
1253
- pid: process.pid,
1254
- ...(currentProcessStartTicks == null
1255
- ? {}
1256
- : { process_start_ticks: currentProcessStartTicks }),
1257
- created_at_ms: now(),
1258
- };
1259
- const tempPath = path.join(
1260
- runDir,
1261
- `.writer.${process.pid}.${writerInstanceID}.tmp`
1262
- );
1263
- try {
1264
- writeDurableTempFile(
1265
- tempPath,
1266
- Buffer.from(`${JSON.stringify(lock)}\n`, 'utf8')
1267
- );
1268
- try {
1269
- fileSystem.linkSync(tempPath, writerLockPath);
1270
- writerLockOwned = true;
1271
- writerLockDirectory = runDir;
1272
- if (runDir === canonicalRunDir) everOwnedCanonicalWriter = true;
1273
- syncRunDirectory();
1274
- } catch (error) {
1275
- if (error?.code !== 'EEXIST') throw error;
1276
- // Producers never delete or replace a lock they did not publish. Only
1277
- // the shell-server can prove owner death and finalize the generation;
1278
- // this removes path-ABA races between concurrent JS successors.
1279
- validateWriterLock(readRegularJSON(writerLockPath, 'writer lock'));
1280
- throw writerBusyError();
1281
- }
1282
- } finally {
1283
- let removed = false;
1284
- try {
1285
- fileSystem.rmSync(tempPath, { force: true });
1286
- removed = true;
1287
- } catch {
1288
- // A stale temp file is harmless and never consumed by the reader.
1289
- }
1290
- if (removed) syncRunDirectory();
1291
- }
1292
- }
1293
-
1294
- function ensureEmptyEventsFile() {
1295
- if (fileSystem.existsSync(eventsPath)) {
1296
- const stat = fileSystem.lstatSync(eventsPath);
1297
- if (!stat.isFile() || stat.isSymbolicLink()) {
1298
- throw new Error(`unsafe preview phase outbox path: ${eventsPath}`);
1299
- }
1300
- return;
1301
- }
1302
- const flags =
1303
- fs.constants.O_CREAT |
1304
- fs.constants.O_EXCL |
1305
- fs.constants.O_RDWR |
1306
- (fs.constants.O_NOFOLLOW || 0);
1307
- const fd = fileSystem.openSync(eventsPath, flags, 0o600);
1308
- try {
1309
- fileSystem.fchmodSync(fd, 0o600);
1310
- fileSystem.fdatasyncSync(fd);
1311
- } finally {
1312
- fileSystem.closeSync(fd);
1313
- }
1314
- syncRunDirectory();
1315
- }
1316
-
1317
- function validateOwnedWriterLock() {
1318
- const lock = validateWriterLock(
1319
- readRegularJSON(writerLockPath, 'writer lock')
1320
- );
1321
- if (
1322
- lock.writer_instance_id !== writerInstanceID ||
1323
- lock.pid !== process.pid ||
1324
- (lock.process_start_ticks || '') !== (currentProcessStartTicks || '')
1325
- ) {
1326
- throw writerBusyError();
1327
- }
1328
- writerLockOwned = true;
1329
- writerLockDirectory = runDir;
1330
- }
1331
-
1332
- let stagingOwned = false;
1333
-
1334
- function tryAdoptCanonicalWriter() {
1335
- const previousDirectory = runDir;
1336
- const previousWriterLockOwned = writerLockOwned;
1337
- const previousWriterLockDirectory = writerLockDirectory;
1338
- try {
1339
- selectRunDirectory(canonicalRunDir);
1340
- assertExistingSafeDirectory(fileSystem, canonicalRunDir);
1341
- metadata = readAndValidateMetadata();
1342
- writerLockOwned = false;
1343
- writerLockDirectory = null;
1344
- validateOwnedWriterLock();
1345
- stagingOwned = false;
1346
- everOwnedCanonicalWriter = true;
1347
- return true;
1348
- } catch {
1349
- selectRunDirectory(previousDirectory);
1350
- writerLockOwned = previousWriterLockOwned;
1351
- writerLockDirectory = previousWriterLockDirectory;
1352
- return false;
1353
- }
1354
- }
1355
-
1356
- function ensureCanonicalPublicationDurable() {
1357
- if (canonicalPublicationDurable) return;
1358
- try {
1359
- syncRootDirectory();
1360
- } catch (firstError) {
1361
- // rename may have committed even when its first parent-directory fsync
1362
- // failed. Re-prove the exact writer identity, then retry that commit
1363
- // point before any event or producer marker can be published.
1364
- if (!tryAdoptCanonicalWriter()) throw firstError;
1365
- syncRootDirectory();
1366
- }
1367
- canonicalPublicationDurable = true;
1368
- }
1369
-
1370
- function registerCanonicalWriter(rejectedEvent) {
1371
- if (producerRegistrationDurable) return;
1372
- producerStartedAtMs = producerStartedAtMs ?? now();
1373
- const rejectedEventForRegistration =
1374
- rejectedEvent?.schema_version === 1 &&
1375
- typeof rejectedEvent.event_id === 'string' &&
1376
- rejectedEvent.event_id.startsWith(`${options.run_id}:`) &&
1377
- rejectedEvent.run_id === options.run_id &&
1378
- PRODUCER_INSTANCE_ID_PATTERN.test(
1379
- rejectedEvent.producer_instance_id || ''
1380
- ) &&
1381
- rejectedEvent.producer_seq === 1 &&
1382
- rejectedEvent.transport === 'outbox'
1383
- ? rejectedEvent
1384
- : null;
1385
- const request = {
1386
- // root/staging_name/started_at_ms are local callback context only. The
1387
- // default registrar serializes the fixed protocol whitelist below and
1388
- // never sends a caller-controlled path to shell-server.
1389
- root,
1390
- staging_name: stagingName,
1391
- run_id: options.run_id,
1392
- generation: metadata.generation,
1393
- expected_instance_id: expectedInstanceID,
1394
- producer_epoch: producerEpoch,
1395
- writer_instance_id: writerInstanceID,
1396
- pid: process.pid,
1397
- process_start_ticks: currentProcessStartTicks,
1398
- started_at_ms: producerStartedAtMs,
1399
- ...(rejectedEventForRegistration == null
1400
- ? {}
1401
- : {
1402
- producer_instance_id:
1403
- rejectedEventForRegistration.producer_instance_id,
1404
- fallback_session_id: fallbackSessionID,
1405
- rejected_event: rejectedEventForRegistration,
1406
- }),
1407
- };
1408
- if (producerRegistrationAttemptCount >= PRODUCER_REGISTER_ATTEMPTS) {
1409
- throw producerRegistrationLastError;
1410
- }
1411
- while (producerRegistrationAttemptCount < PRODUCER_REGISTER_ATTEMPTS) {
1412
- producerRegistrationAttemptCount += 1;
1413
- try {
1414
- const response = validateProducerRegistrationResponse(
1415
- registerProducer(request),
1416
- request
1417
- );
1418
- if (response.registration_status === 'rejected') {
1419
- request.rejected_event.transport = 'fallback_spool';
1420
- request.rejected_event.fallback_session_id = fallbackSessionID;
1421
- request.rejected_event.fallback_seq = 1;
1422
- fallbackRequired = true;
1423
- fallbackSeq = 1;
1424
- fallbackSealed = true;
1425
- sealRequested = true;
1426
- closed = true;
1427
- closeResult = true;
1428
- writerLockOwned = false;
1429
- writerLockDirectory = null;
1430
- stagingOwned = false;
1431
- producerRegistrationDurable = true;
1432
- selectRunDirectory(canonicalRunDir);
1433
- return;
1434
- }
1435
- if (!tryAdoptCanonicalWriter()) {
1436
- throw producerRegistrationError(
1437
- 'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN',
1438
- 'authoritative registration did not publish the expected canonical writer'
1439
- );
1440
- }
1441
- // A successful authoritative response is issued only after the
1442
- // canonical rename and root-directory fsync. Re-fsyncing here would
1443
- // add latency without strengthening the controller's commit proof.
1444
- canonicalPublicationDurable = true;
1445
- const marker = readAndValidateProducerMarker();
1446
- if (!markerBelongsToThisWriter(marker) || marker.state !== 'active') {
1447
- throw producerRegistrationError(
1448
- 'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN',
1449
- 'authoritative registration did not publish the expected active marker'
1450
- );
1451
- }
1452
- producerStartedAtMs = marker.started_at_ms;
1453
- producerMarkerActive = true;
1454
- producerRegistrationDurable = true;
1455
- return;
1456
- } catch (error) {
1457
- producerRegistrationLastError = error;
1458
- if (
1459
- error?.code === 'PREVIEW_PHASE_PRODUCER_EPOCH_CONFLICT' &&
1460
- tryAdoptCanonicalWriter()
1461
- ) {
1462
- canonicalPublicationDurable = true;
1463
- }
1464
- if (
1465
- error?.code === 'PREVIEW_PHASE_PRODUCER_REGISTRATION_ACK_UNKNOWN' &&
1466
- producerRegistrationAttemptCount < PRODUCER_REGISTER_ATTEMPTS
1467
- ) {
1468
- continue;
1469
- }
1470
- throw error;
1471
- }
1472
- }
1473
- throw producerRegistrationLastError;
1474
- }
1475
-
1476
- function ensureCanonicalWriter(rejectedEvent) {
1477
- if (producerRegistrationDurable && runDir === canonicalRunDir && metadata) {
1478
- ensureCanonicalPublicationDurable();
1479
- return;
1480
- }
1481
-
1482
- if (!stagingOwned && fileSystem.existsSync(canonicalRunDir)) {
1483
- // Existing canonical state is controller-owned. A JS successor may
1484
- // inspect it to produce a precise fail-closed reason, but it must never
1485
- // acquire/remove its writer lock or republish it under a captured epoch.
1486
- selectRunDirectory(canonicalRunDir);
1487
- assertExistingSafeDirectory(fileSystem, canonicalRunDir);
1488
- metadata = readAndValidateMetadata();
1489
- if (fileSystem.existsSync(sealPath)) {
1490
- assertGenerationIsOpen();
1491
- }
1492
- if (fileSystem.existsSync(writerLockPath)) {
1493
- validateWriterLock(readRegularJSON(writerLockPath, 'writer lock'));
1494
- throw writerBusyError();
1495
- }
1496
- throw runConflictError();
1497
- }
1498
-
1499
- selectRunDirectory(stagingRunDir);
1500
- if (!stagingOwned) {
1501
- fileSystem.mkdirSync(stagingRunDir, { mode: 0o700 });
1502
- stagingOwned = true;
1503
- syncRootDirectory();
1504
- }
1505
- assertSafeDirectory(fileSystem, stagingRunDir);
1506
- ensureMetadata();
1507
- if (!ownsSelectedWriterLock()) acquireWriterLock();
1508
- ensureEmptyEventsFile();
1509
- syncRunDirectory();
1510
- registerCanonicalWriter(rejectedEvent);
1511
- }
1512
-
1513
- function assertStateGeneration(state, description) {
1514
- if (
1515
- state.run_id !== options.run_id ||
1516
- state.generation !== metadata.generation
1517
- ) {
1518
- const error = new Error(
1519
- `preview phase ${description} generation mismatch: ${options.run_id}`
1520
- );
1521
- error.code = 'PREVIEW_PHASE_GENERATION_MISMATCH';
1522
- throw error;
1523
- }
1524
- }
1525
-
1526
- function assertGenerationIsOpen() {
1527
- if (!fileSystem.existsSync(sealPath)) return;
1528
- readAndValidateSeal();
1529
- const error = new Error(
1530
- `preview phase sealed generation cannot be reopened: ${options.run_id}`
1531
- );
1532
- error.code = 'PREVIEW_PHASE_GENERATION_SEALED';
1533
- throw error;
1534
- }
1535
-
1536
- function readAndValidateSeal() {
1537
- const seal = readRegularJSON(sealPath, 'seal');
1538
- assertStateGeneration(seal, 'seal');
1539
- if (
1540
- seal.protocol !== 'preview-phase-seal-v1' ||
1541
- seal.schema_version !== 1 ||
1542
- !Number.isSafeInteger(seal.final_size_bytes) ||
1543
- seal.final_size_bytes < 0 ||
1544
- !Number.isFinite(seal.sealed_at_ms) ||
1545
- seal.sealed_at_ms <= 0 ||
1546
- (seal.fallback_required !== undefined &&
1547
- typeof seal.fallback_required !== 'boolean')
1548
- ) {
1549
- throw new Error(`invalid preview phase seal: ${sealPath}`);
1550
- }
1551
- return seal;
1552
- }
1553
-
1554
- function observeDegradedMarker() {
1555
- if (!fileSystem.existsSync(degradedPath)) return false;
1556
- const marker = readRegularJSON(degradedPath, 'degraded marker');
1557
- assertStateGeneration(marker, 'degraded marker');
1558
- if (
1559
- marker.protocol !== 'preview-phase-degraded-v1' ||
1560
- marker.schema_version !== 1 ||
1561
- typeof marker.first_failed_event_id !== 'string' ||
1562
- !Number.isFinite(marker.degraded_at_ms) ||
1563
- marker.degraded_at_ms <= 0 ||
1564
- !STATE_ID_PATTERN.test(marker.reason || '')
1565
- ) {
1566
- throw new Error(`invalid preview phase degraded marker: ${degradedPath}`);
1567
- }
1568
- degraded = true;
1569
- fallbackRequired = true;
1570
- return true;
1571
- }
1572
-
1573
- function observeFallbackIntent() {
1574
- if (!fileSystem.existsSync(fallbackIntentPath)) return false;
1575
- validateFallbackIntent(
1576
- readRegularJSON(fallbackIntentPath, 'fallback intent'),
1577
- metadata
1578
- );
1579
- fallbackRequired = true;
1580
- return true;
1581
- }
1582
-
1583
- function assertGenerationIsNotDegraded() {
1584
- if (!observeDegradedMarker()) return;
1585
- throw degradedError();
1586
- }
1587
-
1588
- function removeOwnedWriterLock() {
1589
- const lock = readRegularJSON(writerLockPath, 'writer lock');
1590
- if (
1591
- lock.protocol !== 'preview-phase-writer-v1' ||
1592
- lock.schema_version !== 1 ||
1593
- lock.run_id !== options.run_id ||
1594
- lock.generation !== metadata.generation ||
1595
- lock.writer_instance_id !== writerInstanceID
1596
- ) {
1597
- throw new Error('preview phase writer lock ownership changed');
1598
- }
1599
- fileSystem.rmSync(writerLockPath);
1600
- writerLockOwned = false;
1601
- writerLockDirectory = null;
1602
- syncRunDirectory();
1603
- }
1604
-
1605
- function recoverTornTail() {
1606
- if (recovered) return;
1607
- if (!fileSystem.existsSync(eventsPath)) {
1608
- recovered = true;
1609
- return;
1610
- }
1611
- const readFlags = fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW || 0);
1612
- const fd = fileSystem.openSync(eventsPath, readFlags);
1613
- try {
1614
- const size = fileSystem.fstatSync(fd).size;
1615
- if (size === 0) {
1616
- recovered = true;
1617
- return;
1618
- }
1619
- const tailByte = Buffer.allocUnsafe(1);
1620
- fileSystem.readSync(fd, tailByte, 0, 1, size - 1);
1621
- if (tailByte[0] === 0x0a) {
1622
- recovered = true;
1623
- return;
1624
- }
1625
-
1626
- const chunk = Buffer.allocUnsafe(64 * 1024);
1627
- let scanEnd = size;
1628
- while (scanEnd > 0) {
1629
- const scanStart = Math.max(0, scanEnd - chunk.length);
1630
- const length = scanEnd - scanStart;
1631
- const bytesRead = fileSystem.readSync(fd, chunk, 0, length, scanStart);
1632
- const lastNewline = chunk.lastIndexOf(0x0a, bytesRead - 1);
1633
- if (lastNewline >= 0) {
1634
- fileSystem.ftruncateSync(fd, scanStart + lastNewline + 1);
1635
- recovered = true;
1636
- return;
1637
- }
1638
- scanEnd = scanStart;
1639
- }
1640
- fileSystem.ftruncateSync(fd, 0);
1641
- recovered = true;
1642
- } finally {
1643
- fileSystem.closeSync(fd);
1644
- }
1645
- }
1646
-
1647
- function degradedError() {
1648
- const error = new Error('preview phase outbox is degraded');
1649
- error.code = 'PREVIEW_PHASE_OUTBOX_DEGRADED';
1650
- return error;
1651
- }
1652
-
1653
- function latchDegraded(event, cause) {
1654
- fallbackRequired = true;
1655
- if (degraded) return;
1656
- degraded = true;
1657
- if (!metadata || !ownsSelectedWriterLock()) return;
1658
- const reason =
1659
- typeof cause?.code === 'string' && STATE_ID_PATTERN.test(cause.code)
1660
- ? cause.code
1661
- : 'append_failed';
1662
- try {
1663
- publishImmutableState(degradedPath, 'degraded', {
1664
- protocol: 'preview-phase-degraded-v1',
1665
- schema_version: 1,
1666
- run_id: options.run_id,
1667
- generation: metadata.generation,
1668
- first_failed_event_id:
1669
- typeof event?.event_id === 'string'
1670
- ? event.event_id
1671
- : `${options.run_id}:transport-fallback`,
1672
- degraded_at_ms: now(),
1673
- reason,
1674
- });
1675
- } catch {
1676
- // A total filesystem failure cannot be made durable. The in-memory latch
1677
- // still prevents this writer from creating holes in the JSONL prefix.
1678
- }
1679
- }
1680
-
1681
- function finalizeUnboundFallback(event, conflict) {
1682
- fallbackRequired = true;
1683
- latchDegraded(event, conflict);
1684
- recoverTornTail();
1685
- ensureEmptyEventsFile();
1686
- const fd = fileSystem.openSync(
1687
- eventsPath,
1688
- fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
1689
- );
1690
- let finalSize;
1691
- try {
1692
- finalSize = fileSystem.fstatSync(fd).size;
1693
- } finally {
1694
- fileSystem.closeSync(fd);
1695
- }
1696
- const ownedWriter = validateWriterLock(
1697
- readRegularJSON(writerLockPath, 'writer lock')
1698
- );
1699
- if (
1700
- ownedWriter.writer_instance_id !== writerInstanceID ||
1701
- ownedWriter.pid !== process.pid ||
1702
- (ownedWriter.process_start_ticks || '') !==
1703
- (currentProcessStartTicks || '')
1704
- ) {
1705
- throw writerBusyError();
1706
- }
1707
- const terminalAt = Math.max(now(), ownedWriter.created_at_ms);
1708
- if (!fileSystem.existsSync(sealPath)) {
1709
- publishImmutableState(sealPath, 'sealed', {
1710
- protocol: 'preview-phase-seal-v1',
1711
- schema_version: 1,
1712
- run_id: options.run_id,
1713
- generation: metadata.generation,
1714
- final_size_bytes: finalSize,
1715
- sealed_at_ms: terminalAt,
1716
- fallback_required: true,
1717
- });
1718
- }
1719
- const observedOwner = readAndValidateProducerMarker();
1720
- if (!fileSystem.existsSync(unboundFallbackPath)) {
1721
- publishImmutableState(unboundFallbackPath, 'producer-unbound', {
1722
- protocol: 'preview-phase-unbound-fallback-v1',
1723
- schema_version: 1,
1724
- epoch: producerEpoch,
1725
- run_id: options.run_id,
1726
- generation: metadata.generation,
1727
- writer_instance_id: writerInstanceID,
1728
- pid: process.pid,
1729
- process_start_ticks: currentProcessStartTicks,
1730
- started_at_ms: ownedWriter.created_at_ms,
1731
- state: 'terminal',
1732
- terminal_at_ms: terminalAt,
1733
- observed_owner_run_id: observedOwner.run_id,
1734
- observed_owner_generation: observedOwner.generation,
1735
- });
1736
- }
1737
- if (ownsSelectedWriterLock()) removeOwnedWriterLock();
1738
- closed = true;
1739
- closeResult = true;
1740
- }
1741
-
1742
- function finalizeUnpublishedStagingFallback(event, conflict) {
1743
- if (
1744
- runDir !== stagingRunDir ||
1745
- !stagingOwned ||
1746
- !ownsSelectedWriterLock()
1747
- ) {
1748
- return false;
1749
- }
1750
- ensureMetadata();
1751
- fallbackRequired = true;
1752
- latchDegraded(event, conflict);
1753
- recoverTornTail();
1754
- ensureEmptyEventsFile();
1755
- const fd = fileSystem.openSync(
1756
- eventsPath,
1757
- fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
1758
- );
1759
- let finalSize;
1760
- try {
1761
- finalSize = fileSystem.fstatSync(fd).size;
1762
- } finally {
1763
- fileSystem.closeSync(fd);
1764
- }
1765
- const ownedWriter = validateWriterLock(
1766
- readRegularJSON(writerLockPath, 'writer lock')
1767
- );
1768
- if (
1769
- ownedWriter.writer_instance_id !== writerInstanceID ||
1770
- ownedWriter.pid !== process.pid ||
1771
- (ownedWriter.process_start_ticks || '') !==
1772
- (currentProcessStartTicks || '')
1773
- ) {
1774
- throw writerBusyError();
1775
- }
1776
- if (!fileSystem.existsSync(sealPath)) {
1777
- publishImmutableState(sealPath, 'sealed', {
1778
- protocol: 'preview-phase-seal-v1',
1779
- schema_version: 1,
1780
- run_id: options.run_id,
1781
- generation: metadata.generation,
1782
- final_size_bytes: finalSize,
1783
- sealed_at_ms: Math.max(now(), ownedWriter.created_at_ms),
1784
- fallback_required: true,
1785
- });
1786
- }
1787
- removeOwnedWriterLock();
1788
- closed = true;
1789
- closeResult = true;
1790
- try {
1791
- const allowed = new Set([
1792
- 'meta.json',
1793
- 'events.jsonl',
1794
- 'degraded.json',
1795
- 'sealed.json',
1796
- ]);
1797
- const entries = readDirectoryEntriesBounded(
1798
- stagingRunDir,
1799
- allowed.size,
1800
- 'preview phase unpublished staging exceeds cleanup entry limit'
1801
- );
1802
- for (const entry of entries) {
1803
- if (!allowed.has(entry.name)) {
1804
- throw new Error(
1805
- 'preview phase unpublished staging contains unknown state'
1806
- );
1807
- }
1808
- const entryPath = path.join(stagingRunDir, entry.name);
1809
- const stat = fileSystem.lstatSync(entryPath);
1810
- if (!stat.isFile() || stat.isSymbolicLink()) {
1811
- throw new Error(
1812
- 'preview phase unpublished staging contains unsafe state'
1813
- );
1814
- }
1815
- fileSystem.rmSync(entryPath);
1816
- }
1817
- syncRunDirectory();
1818
- fileSystem.rmdirSync(stagingRunDir);
1819
- syncRootDirectory();
1820
- stagingOwned = false;
1821
- } catch {
1822
- // The writer lock is already durably absent. Trusted controller GC can
1823
- // finish an unexpected cleanup remainder without touching canonical.
1824
- }
1825
- return true;
1826
- }
1827
-
1828
- function append(event) {
1829
- if (closed) throw new Error('preview phase outbox is closed');
1830
- if (degraded) throw degradedError();
1831
- const payload = JSON.stringify(event);
1832
- if (Buffer.byteLength(payload, 'utf8') > MAX_EVENT_BYTES) {
1833
- const error = new Error(
1834
- `preview phase event exceeds ${MAX_EVENT_BYTES} bytes`
1835
- );
1836
- error.code = 'PREVIEW_PHASE_EVENT_TOO_LARGE';
1837
- throw error;
1838
- }
1839
- const record = Buffer.from(`${payload}\n`, 'utf8');
1840
- for (let attempt = 1; attempt <= MAX_TRANSIENT_ATTEMPTS; attempt += 1) {
1841
- try {
1842
- assertSafeDirectory(fileSystem, root);
1843
- assertRootDirectoryQuota();
1844
- ensureCanonicalWriter(event);
1845
- if (
1846
- closed &&
1847
- producerRegistrationDurable &&
1848
- !everOwnedCanonicalWriter
1849
- ) {
1850
- return true;
1851
- }
1852
- // The authoritative registrar proves the initial writer identity, but
1853
- // every append must still fail closed if a controller or an unexpected
1854
- // same-UID actor replaces auxiliary state after registration.
1855
- validateOwnedWriterLock();
1856
- assertGenerationIsOpen();
1857
- publishActiveProducerMarker();
1858
- // Any existing writer identity is authoritative to JS. Dead-owner
1859
- // recovery and torn-tail finalization belong exclusively to shell-server.
1860
- assertGenerationIsNotDegraded();
1861
- assertRootByteQuota(record.length);
1862
- if (eventsFD == null) {
1863
- if (fileSystem.existsSync(eventsPath)) {
1864
- const stat = fileSystem.lstatSync(eventsPath);
1865
- if (!stat.isFile() || stat.isSymbolicLink()) {
1866
- throw new Error(
1867
- `unsafe preview phase outbox path: ${eventsPath}`
1868
- );
1869
- }
1870
- }
1871
- recoverTornTail();
1872
- const flags =
1873
- fs.constants.O_CREAT |
1874
- fs.constants.O_WRONLY |
1875
- fs.constants.O_APPEND |
1876
- (fs.constants.O_NOFOLLOW || 0);
1877
- eventsFD = fileSystem.openSync(eventsPath, flags, 0o600);
1878
- fileSystem.fchmodSync(eventsFD, 0o600);
1879
- committedBytes = fileSystem.fstatSync(eventsFD).size;
1880
- }
1881
- if (committedBytes + record.length > maxRunBytes) {
1882
- const error = new Error(
1883
- `preview phase run quota would exceed ${maxRunBytes} bytes`
1884
- );
1885
- error.code = 'PREVIEW_PHASE_RUN_QUOTA_EXCEEDED';
1886
- throw error;
1887
- }
1888
- let offset = 0;
1889
- while (offset < record.length) {
1890
- const written = fileSystem.writeSync(
1891
- eventsFD,
1892
- record,
1893
- offset,
1894
- record.length - offset
1895
- );
1896
- if (!Number.isInteger(written) || written <= 0) {
1897
- throw new Error('preview phase outbox write made no progress');
1898
- }
1899
- offset += written;
1900
- }
1901
- committedBytes += record.length;
1902
- return true;
1903
- } catch (error) {
1904
- if (eventsFD != null) {
1905
- try {
1906
- fileSystem.closeSync(eventsFD);
1907
- } catch {
1908
- // Best-effort fd cleanup after append failure.
1909
- }
1910
- eventsFD = null;
1911
- committedBytes = null;
1912
- recovered = false;
1913
- }
1914
- if (error?.code === 'PREVIEW_PHASE_PRODUCER_EPOCH_CONFLICT') {
1915
- try {
1916
- finalizeUnboundFallback(event, error);
1917
- } catch {
1918
- // The controller can finish a dead CAS loser from its immutable
1919
- // writer identity. Preserve the original conflict for fallback.
1920
- }
1921
- throw error;
1922
- }
1923
- if (error?.code === 'PREVIEW_PHASE_RUN_CONFLICT') {
1924
- try {
1925
- finalizeUnpublishedStagingFallback(event, error);
1926
- } catch {
1927
- // Preserve the staging writer for controller-owned dead-owner GC.
1928
- }
1929
- throw error;
1930
- }
1931
- if (
1932
- isTransientFilesystemError(error) &&
1933
- attempt < MAX_TRANSIENT_ATTEMPTS
1934
- ) {
1935
- continue;
1936
- }
1937
- latchDegraded(event, error);
1938
- throw error;
1939
- }
1940
- }
1941
- throw new Error('preview phase transient retry loop exhausted');
1942
- }
1943
-
1944
- function flush() {
1945
- if (eventsFD == null) return true;
1946
- for (let attempt = 1; attempt <= MAX_TRANSIENT_ATTEMPTS; attempt += 1) {
1947
- try {
1948
- fileSystem.fdatasyncSync(eventsFD);
1949
- return true;
1950
- } catch (error) {
1951
- if (
1952
- isTransientFilesystemError(error) &&
1953
- attempt < MAX_TRANSIENT_ATTEMPTS
1954
- ) {
1955
- continue;
1956
- }
1957
- latchDegraded(null, error);
1958
- throw error;
1959
- }
1960
- }
1961
- throw new Error('preview phase flush retry loop exhausted');
1962
- }
1963
-
1964
- function pollQuiesce() {
1965
- const state = (localSealed, canonicalAcknowledged) => ({
1966
- local_sealed: localSealed,
1967
- canonical_acknowledged: canonicalAcknowledged,
1968
- });
1969
- if (closed) {
1970
- return state(closeResult, everOwnedCanonicalWriter && closeResult);
1971
- }
1972
- if (sealRequested) {
1973
- const localSealed = close();
1974
- return state(localSealed, everOwnedCanonicalWriter && localSealed);
1975
- }
1976
- if (!metadata) return state(false, false);
1977
- if (!fileSystem.existsSync(quiesceRequestPath)) return state(false, false);
1978
- const request = readRegularJSON(quiesceRequestPath, 'quiesce request');
1979
- assertStateGeneration(request, 'quiesce request');
1980
- if (
1981
- request.protocol !== 'preview-phase-quiesce-v1' ||
1982
- request.schema_version !== 1 ||
1983
- !STATE_ID_PATTERN.test(request.request_id || '') ||
1984
- !Number.isFinite(request.requested_at_ms) ||
1985
- request.requested_at_ms <= 0
1986
- ) {
1987
- throw new Error(
1988
- `invalid preview phase quiesce request: ${quiesceRequestPath}`
1989
- );
1990
- }
1991
- if (!ownsSelectedWriterLock()) {
1992
- // Do not acknowledge or mutate the canonical owner lifecycle. The
1993
- // quiesce boundary still terminalizes this producer's own fallback file.
1994
- sealRequested = true;
1995
- closeResult = sealFallback();
1996
- closed = true;
1997
- return state(closeResult, false);
1998
- }
1999
- const localSealed = close();
2000
- return state(localSealed, localSealed);
2001
- }
2002
-
2003
- function close() {
2004
- const fallbackCloseResult = sealFallback();
2005
- // A fallback-only producer never owns the canonical writer lifecycle.
2006
- // Its open spool fd is sufficient to terminalize after controller rename;
2007
- // attempting canonical close here could recreate or mutate another owner.
2008
- if (fallbackStarted && !everOwnedCanonicalWriter) {
2009
- closed = true;
2010
- closeResult = fallbackCloseResult;
2011
- return closeResult;
2012
- }
2013
- if (closed) return closeResult && fallbackCloseResult;
2014
- sealRequested = true;
2015
- let finalSize = null;
2016
- try {
2017
- // A reporter that never attempted persistence has no generation to
2018
- // close. Persistence failures set degraded/fallbackRequired and retry
2019
- // this initialization when the filesystem becomes available again.
2020
- if (!metadata && !degraded && !fallbackRequired) {
2021
- closed = true;
2022
- closeResult = fallbackCloseResult;
2023
- return closeResult;
2024
- }
2025
- assertSafeDirectory(fileSystem, root);
2026
- assertRootDirectoryQuota();
2027
- ensureCanonicalWriter();
2028
-
2029
- if (fileSystem.existsSync(sealPath)) {
2030
- const seal = readAndValidateSeal();
2031
- fallbackRequired ||= seal.fallback_required === true;
2032
- if (ownsSelectedWriterLock()) {
2033
- publishActiveProducerMarker();
2034
- publishTerminalProducerMarker();
2035
- removeOwnedWriterLock();
2036
- } else if (fileSystem.existsSync(writerLockPath)) {
2037
- closeResult = false;
2038
- return false;
2039
- } else {
2040
- const marker = readAndValidateProducerMarker();
2041
- if (
2042
- !markerBelongsToThisWriter(marker) ||
2043
- marker.state !== 'terminal'
2044
- ) {
2045
- closeResult = false;
2046
- return false;
2047
- }
2048
- producerStartedAtMs = marker.started_at_ms;
2049
- producerTerminalAtMs = marker.terminal_at_ms;
2050
- producerMarkerActive = true;
2051
- producerMarkerTerminal = true;
2052
- }
2053
- closed = true;
2054
- closeResult = fallbackCloseResult;
2055
- return closeResult;
2056
- }
2057
-
2058
- if (!ownsSelectedWriterLock()) {
2059
- acquireWriterLock();
2060
- }
2061
- publishActiveProducerMarker();
2062
- observeFallbackIntent();
2063
- observeDegradedMarker();
2064
- if (eventsFD != null) {
2065
- fileSystem.fdatasyncSync(eventsFD);
2066
- finalSize = fileSystem.fstatSync(eventsFD).size;
2067
- fileSystem.closeSync(eventsFD);
2068
- eventsFD = null;
2069
- committedBytes = null;
2070
- } else {
2071
- recoverTornTail();
2072
- // The first append can fail after the writer lock is acquired but
2073
- // before events.jsonl is created (for example, aggregate quota). Such
2074
- // a degraded generation still needs an empty, sealed prefix so the
2075
- // lock is releasable and the reader can merge stdout_fallback safely.
2076
- const flags =
2077
- fs.constants.O_CREAT |
2078
- fs.constants.O_RDWR |
2079
- (fs.constants.O_NOFOLLOW || 0);
2080
- const fd = fileSystem.openSync(eventsPath, flags, 0o600);
2081
- try {
2082
- fileSystem.fchmodSync(fd, 0o600);
2083
- fileSystem.fdatasyncSync(fd);
2084
- finalSize = fileSystem.fstatSync(fd).size;
2085
- } finally {
2086
- fileSystem.closeSync(fd);
2087
- }
2088
- }
2089
- publishImmutableState(sealPath, 'sealed', {
2090
- protocol: 'preview-phase-seal-v1',
2091
- schema_version: 1,
2092
- run_id: options.run_id,
2093
- generation: metadata.generation,
2094
- final_size_bytes: finalSize,
2095
- sealed_at_ms: now(),
2096
- fallback_required: fallbackRequired,
2097
- });
2098
- publishTerminalProducerMarker();
2099
- removeOwnedWriterLock();
2100
- closeResult = fallbackCloseResult;
2101
- closed = true;
2102
- } catch (error) {
2103
- closeResult = false;
2104
- if (!isTransientFilesystemError(error)) {
2105
- latchDegraded(null, error);
2106
- }
2107
- if (eventsFD != null) {
2108
- try {
2109
- fileSystem.closeSync(eventsFD);
2110
- } catch {
2111
- // Best-effort fd cleanup after close failure.
2112
- }
2113
- eventsFD = null;
2114
- committedBytes = null;
2115
- }
2116
- // Keep the unique writer owner and pending close intent for retries. If
2117
- // the process dies, shell-server is the sole authority that verifies
2118
- // PID+starttime, repairs the tail, and seals the generation.
2119
- }
2120
- return closeResult;
2121
- }
2122
-
2123
- return {
2124
- append,
2125
- append_fallback: appendFallback,
2126
- seal_fallback: sealFallback,
2127
- flush,
2128
- close,
2129
- poll_quiesce: pollQuiesce,
2130
- is_local_sealed: () => closed && closeResult,
2131
- events_path: eventsPath,
2132
- meta_path: metaPath,
2133
- seal_path: sealPath,
2134
- degraded_path: degradedPath,
2135
- quiesce_request_path: quiesceRequestPath,
2136
- producer_epoch_marker_path: producerEpochMarkerPath,
2137
- fallback_spool_path: fallbackSpoolPath,
2138
- };
2139
- }
2140
-
2141
- module.exports = {
2142
- DEFAULT_OUTBOX_ROOT,
2143
- DEFAULT_MAX_RUN_BYTES,
2144
- DEFAULT_MAX_ROOT_BYTES,
2145
- DEFAULT_MAX_ROOT_DIRECTORIES,
2146
- DEFAULT_MAX_ROOT_SCAN_DIRECTORIES,
2147
- DEFAULT_MAX_RUN_SCAN_ENTRIES,
2148
- DEFAULT_MAX_FALLBACK_FILES,
2149
- DEFAULT_MAX_FALLBACK_BYTES,
2150
- DEFAULT_MAX_FALLBACK_EVENTS,
2151
- MAX_FALLBACK_RECORD_BYTES,
2152
- MAX_EVENT_BYTES,
2153
- createFallbackSessionID,
2154
- createPreviewPhaseOutbox,
2155
- };