@lark-apaas/fullstack-cli 1.1.59-alpha.20260719124023 → 1.1.59-alpha.20260720153322

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/dist/index.js CHANGED
@@ -8660,6 +8660,7 @@ function previewArtifactBuildEnvironment(environment, appBasePath) {
8660
8660
  buildEnvironment[key] = value;
8661
8661
  }
8662
8662
  }
8663
+ buildEnvironment.ASSETS_CDN_PATH = appBasePath;
8663
8664
  buildEnvironment.CLIENT_BASE_PATH = appBasePath;
8664
8665
  buildEnvironment.NODE_ENV = environment?.NODE_ENV ?? "production";
8665
8666
  return buildEnvironment;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/fullstack-cli",
3
- "version": "1.1.59-alpha.20260719124023",
3
+ "version": "1.1.59-alpha.20260720153322",
4
4
  "description": "CLI tool for fullstack template management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -8,7 +8,9 @@ const readline = require('readline');
8
8
  const {
9
9
  applyPreviewArtifactHostOverrides,
10
10
  buildListeningPortLookupArgs,
11
+ closePreviewPhaseReporterWithRetry,
11
12
  createDeferredTypecheckGate,
13
+ createPreviewPhaseFrameDecoder,
12
14
  createPreviewPhaseReporter,
13
15
  normalizeTcpPort,
14
16
  parseTscWatchSummary,
@@ -199,6 +201,7 @@ function startProcess({
199
201
  args,
200
202
  cleanupPort,
201
203
  phaseDetail = {},
204
+ phaseChannel = false,
202
205
  onReady,
203
206
  onOutputLine,
204
207
  }) {
@@ -236,12 +239,23 @@ function startProcess({
236
239
  }
237
240
 
238
241
  while (!stopping) {
242
+ const childEnv = { ...process.env };
243
+ // The parent is the only outbox writer. Child processes receive a narrow
244
+ // producer channel, never the filesystem destination.
245
+ delete childEnv.MIAODA_PREVIEW_PHASE_OUTBOX_ROOT;
246
+ if (phaseChannel) {
247
+ childEnv.MIAODA_PREVIEW_PHASE_FD = '3';
248
+ } else {
249
+ delete childEnv.MIAODA_PREVIEW_PHASE_FD;
250
+ }
239
251
  const child = spawn(command, args, {
240
252
  detached: true,
241
- stdio: ['ignore', 'pipe', 'pipe'],
253
+ stdio: phaseChannel
254
+ ? ['ignore', 'pipe', 'pipe', 'pipe']
255
+ : ['ignore', 'pipe', 'pipe'],
242
256
  shell: true,
243
257
  cwd: PROJECT_ROOT,
244
- env: { ...process.env },
258
+ env: childEnv,
245
259
  });
246
260
 
247
261
  entry.pid = child.pid;
@@ -307,13 +321,24 @@ function startProcess({
307
321
 
308
322
  // Pipe stdout and stderr through readline for timestamped logging
309
323
  let typecheckSummaryReported = false;
310
- const pipeLines = stream => {
324
+ let phaseReported = false;
325
+ const pipeLines = (stream, allowPhaseFallback = false) => {
311
326
  const rl = readline.createInterface({
312
327
  input: stream,
313
328
  crlfDelay: Infinity,
314
329
  });
315
330
  rl.on('line', line => {
316
331
  if (onOutputLine) onOutputLine(line);
332
+ const legacyPhaseCandidate =
333
+ allowPhaseFallback && line.startsWith('[MiaodaPreviewPhase] ');
334
+ if (legacyPhaseCandidate && !phaseReported) {
335
+ phaseReported = previewPhaseReporter.ingest(line, {
336
+ producer: 'vite_plugin',
337
+ allowed_phases: ['vite_http_listening'],
338
+ attempt,
339
+ allow_legacy_stdout: true,
340
+ });
341
+ }
317
342
  if (name === 'server-typecheck' && !typecheckSummaryReported) {
318
343
  const summary = parseTscWatchSummary(line);
319
344
  if (summary) {
@@ -334,11 +359,38 @@ function startProcess({
334
359
  try {
335
360
  fs.writeSync(logFd, msg);
336
361
  } catch {}
337
- writeOutput(msg);
362
+ // Phase fallbacks are normalized by the parent reporter before they
363
+ // enter the shared log. Keep the raw child claim quarantined in the
364
+ // component log so collectors never observe two representations.
365
+ if (!legacyPhaseCandidate) writeOutput(msg);
338
366
  });
339
367
  };
340
- if (child.stdout) pipeLines(child.stdout);
368
+ if (child.stdout) pipeLines(child.stdout, phaseChannel);
341
369
  if (child.stderr) pipeLines(child.stderr);
370
+ if (phaseChannel && child.stdio[3]) {
371
+ let oversizedPhaseFrameReported = false;
372
+ const phaseDecoder = createPreviewPhaseFrameDecoder({
373
+ max_bytes: 16 * 1024,
374
+ on_oversized_frame: () => {
375
+ if (oversizedPhaseFrameReported) return;
376
+ oversizedPhaseFrameReported = true;
377
+ logEvent(
378
+ 'WARN',
379
+ name,
380
+ 'Dropped an oversized preview phase producer frame'
381
+ );
382
+ },
383
+ on_frame: line => {
384
+ if (phaseReported) return;
385
+ phaseReported = previewPhaseReporter.ingest(line, {
386
+ producer: 'vite_plugin',
387
+ allowed_phases: ['vite_http_listening'],
388
+ attempt,
389
+ });
390
+ },
391
+ });
392
+ child.stdio[3].on('data', chunk => phaseDecoder.push(chunk));
393
+ }
342
394
 
343
395
  // Wait for the direct child to exit.
344
396
  // NOTE: must use 'exit', not 'close'. With shell:true, grandchild processes
@@ -468,6 +520,16 @@ async function cleanup() {
468
520
 
469
521
  logEvent('INFO', 'main', 'All processes stopped');
470
522
 
523
+ const previewPhaseSealed =
524
+ await closePreviewPhaseReporterWithRetry(previewPhaseReporter);
525
+ if (!previewPhaseSealed) {
526
+ logEvent(
527
+ 'ERROR',
528
+ 'main',
529
+ 'Preview phase outbox did not seal after bounded shutdown retries'
530
+ );
531
+ }
532
+
471
533
  try {
472
534
  fs.closeSync(devStdLogFd);
473
535
  } catch {}
@@ -475,7 +537,7 @@ async function cleanup() {
475
537
  fs.closeSync(devLogFd);
476
538
  } catch {}
477
539
 
478
- process.exit(0);
540
+ process.exit(previewPhaseSealed ? 0 : 1);
479
541
  }
480
542
 
481
543
  process.on('SIGTERM', cleanup);
@@ -644,6 +706,7 @@ async function main() {
644
706
  command: 'npm',
645
707
  args: ['run', 'dev:client'],
646
708
  cleanupPort: CLIENT_DEV_PORT,
709
+ phaseChannel: true,
647
710
  });
648
711
 
649
712
  writeOutput(`📋 Dev processes running. Press Ctrl+C to stop.\n`);
@@ -657,7 +720,14 @@ async function main() {
657
720
  }
658
721
  }
659
722
 
660
- main().catch(err => {
723
+ main().catch(async err => {
661
724
  console.error('Fatal error:', err);
725
+ const previewPhaseSealed =
726
+ await closePreviewPhaseReporterWithRetry(previewPhaseReporter);
727
+ if (!previewPhaseSealed) {
728
+ console.error(
729
+ 'Preview phase outbox did not seal after bounded fatal-exit retries'
730
+ );
731
+ }
662
732
  process.exit(1);
663
733
  });
@@ -0,0 +1,791 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const DEFAULT_OUTBOX_ROOT = '/tmp/event/miaoda-preview-phase/v1';
7
+ const MAX_EVENT_BYTES = 16 * 1024;
8
+ const DEFAULT_MAX_RUN_BYTES = 2 * 1024 * 1024;
9
+ const DEFAULT_MAX_ROOT_BYTES = 64 * 1024 * 1024;
10
+ const DEFAULT_MAX_ROOT_DIRECTORIES = 128;
11
+ const DEFAULT_MAX_ROOT_SCAN_DIRECTORIES = 256;
12
+ const RUN_ID_PATTERN = /^prv_[A-Za-z0-9_-]{1,60}$/;
13
+ const STATE_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
14
+ const PROCESS_START_TICKS_PATTERN = /^\d+$/;
15
+
16
+ function readLinuxProcessStartTicks(fileSystem, pid) {
17
+ try {
18
+ const stat = fileSystem.readFileSync(`/proc/${pid}/stat`, 'utf8');
19
+ // comm (field 2) may contain spaces or parentheses, so split only after
20
+ // its final closing parenthesis. The remaining first field is state (3),
21
+ // therefore starttime (22) is at index 19.
22
+ const commEnd = stat.lastIndexOf(')');
23
+ if (commEnd < 0) return null;
24
+ const fields = stat
25
+ .slice(commEnd + 1)
26
+ .trim()
27
+ .split(/\s+/);
28
+ const startTicks = fields[19];
29
+ return PROCESS_START_TICKS_PATTERN.test(startTicks || '')
30
+ ? startTicks
31
+ : null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function assertSafeDirectory(fileSystem, directoryPath) {
38
+ if (!fileSystem.existsSync(directoryPath)) {
39
+ fileSystem.mkdirSync(directoryPath, { recursive: true, mode: 0o700 });
40
+ }
41
+ const stat = fileSystem.lstatSync(directoryPath);
42
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
43
+ throw new Error(`unsafe preview phase outbox path: ${directoryPath}`);
44
+ }
45
+ fileSystem.chmodSync(directoryPath, 0o700);
46
+ }
47
+
48
+ function createPreviewPhaseOutbox(options) {
49
+ const fileSystem = options.fs || fs;
50
+ const root = options.root || DEFAULT_OUTBOX_ROOT;
51
+ const randomUUID = options.random_uuid || require('crypto').randomUUID;
52
+ const now = options.now || Date.now;
53
+ const writerInstanceID = options.writer_instance_id || randomUUID();
54
+ const getProcessStartTicks =
55
+ options.get_process_start_ticks ||
56
+ (pid => readLinuxProcessStartTicks(fileSystem, pid));
57
+ const configuredProcessStartTicks = options.process_start_ticks;
58
+ const currentProcessStartTicks =
59
+ configuredProcessStartTicks == null
60
+ ? getProcessStartTicks(process.pid)
61
+ : configuredProcessStartTicks;
62
+ const maxRunBytes = options.max_run_bytes ?? DEFAULT_MAX_RUN_BYTES;
63
+ const maxRootBytes = options.max_root_bytes ?? DEFAULT_MAX_ROOT_BYTES;
64
+ const maxRootDirectories =
65
+ options.max_root_directories ?? DEFAULT_MAX_ROOT_DIRECTORIES;
66
+ const maxRootScanDirectories =
67
+ options.max_root_scan_directories ?? DEFAULT_MAX_ROOT_SCAN_DIRECTORIES;
68
+ if (!RUN_ID_PATTERN.test(options.run_id || '')) {
69
+ throw new Error('invalid preview run_id for phase outbox');
70
+ }
71
+ if (!STATE_ID_PATTERN.test(writerInstanceID)) {
72
+ throw new Error('invalid preview phase writer_instance_id');
73
+ }
74
+ if (
75
+ currentProcessStartTicks != null &&
76
+ !PROCESS_START_TICKS_PATTERN.test(currentProcessStartTicks)
77
+ ) {
78
+ throw new Error('invalid preview phase process_start_ticks');
79
+ }
80
+ if (!Number.isSafeInteger(maxRunBytes) || maxRunBytes <= 0) {
81
+ throw new Error('invalid preview phase max_run_bytes');
82
+ }
83
+ if (!Number.isSafeInteger(maxRootBytes) || maxRootBytes <= 0) {
84
+ throw new Error('invalid preview phase max_root_bytes');
85
+ }
86
+ if (!Number.isSafeInteger(maxRootDirectories) || maxRootDirectories <= 0) {
87
+ throw new Error('invalid preview phase max_root_directories');
88
+ }
89
+ if (
90
+ !Number.isSafeInteger(maxRootScanDirectories) ||
91
+ maxRootScanDirectories < maxRootDirectories
92
+ ) {
93
+ throw new Error('invalid preview phase max_root_scan_directories');
94
+ }
95
+ const runDir = path.join(root, options.run_id);
96
+ const eventsPath = path.join(runDir, 'events.jsonl');
97
+ const metaPath = path.join(runDir, 'meta.json');
98
+ const writerLockPath = path.join(runDir, 'writer.lock');
99
+ const sealPath = path.join(runDir, 'sealed.json');
100
+ const degradedPath = path.join(runDir, 'degraded.json');
101
+ const quiesceRequestPath = path.join(runDir, 'quiesce.request.json');
102
+ let recovered = false;
103
+ let metadata = null;
104
+ let writerLockOwned = false;
105
+ let eventsFD = null;
106
+ let committedBytes = null;
107
+ let degraded = false;
108
+ let fallbackRequired = false;
109
+ let sealRequested = false;
110
+ let closed = false;
111
+ let closeResult = false;
112
+
113
+ function rootQuotaError(message) {
114
+ const error = new Error(message);
115
+ error.code = 'PREVIEW_PHASE_ROOT_QUOTA_EXCEEDED';
116
+ return error;
117
+ }
118
+
119
+ function inspectRootUsage() {
120
+ const entries = fileSystem.readdirSync(root, { withFileTypes: true });
121
+ if (entries.length > maxRootScanDirectories) {
122
+ throw rootQuotaError(
123
+ `preview phase root scan exceeds ${maxRootScanDirectories} entries`
124
+ );
125
+ }
126
+ let directories = 0;
127
+ let bytes = 0;
128
+ const disappearingEntry = error =>
129
+ error?.code === 'ENOENT' || error?.code === 'ENOTDIR';
130
+ for (const entry of entries) {
131
+ const entryPath = path.join(root, entry.name);
132
+ let stat;
133
+ try {
134
+ stat = fileSystem.lstatSync(entryPath);
135
+ } catch (error) {
136
+ // The Go consumer atomically renames and removes acknowledged runs.
137
+ // A root snapshot can therefore contain an entry that is gone by the
138
+ // following lstat; disappearance is successful GC, not degradation.
139
+ if (disappearingEntry(error)) continue;
140
+ throw error;
141
+ }
142
+ if (stat.isSymbolicLink()) {
143
+ throw new Error(`unsafe preview phase outbox path: ${entryPath}`);
144
+ }
145
+ if (stat.isFile()) {
146
+ bytes += stat.size;
147
+ continue;
148
+ }
149
+ if (!stat.isDirectory()) {
150
+ throw new Error(`unsafe preview phase outbox path: ${entryPath}`);
151
+ }
152
+ directories += 1;
153
+ let runEntries;
154
+ try {
155
+ runEntries = fileSystem.readdirSync(entryPath, {
156
+ withFileTypes: true,
157
+ });
158
+ } catch (error) {
159
+ if (disappearingEntry(error)) continue;
160
+ throw error;
161
+ }
162
+ for (const runEntry of runEntries) {
163
+ const statePath = path.join(entryPath, runEntry.name);
164
+ let stateStat;
165
+ try {
166
+ stateStat = fileSystem.lstatSync(statePath);
167
+ } catch (error) {
168
+ if (disappearingEntry(error)) continue;
169
+ throw error;
170
+ }
171
+ if (stateStat.isSymbolicLink() || !stateStat.isFile()) {
172
+ throw new Error(`unsafe preview phase outbox path: ${statePath}`);
173
+ }
174
+ bytes += stateStat.size;
175
+ }
176
+ }
177
+ return { bytes, directories };
178
+ }
179
+
180
+ function assertRootDirectoryQuota() {
181
+ const usage = inspectRootUsage();
182
+ if (
183
+ !fileSystem.existsSync(runDir) &&
184
+ usage.directories >= maxRootDirectories
185
+ ) {
186
+ throw rootQuotaError(
187
+ `preview phase root directory quota reached ${maxRootDirectories}`
188
+ );
189
+ }
190
+ }
191
+
192
+ function assertRootByteQuota(recordBytes) {
193
+ const usage = inspectRootUsage();
194
+ if (usage.bytes + recordBytes > maxRootBytes) {
195
+ throw rootQuotaError(
196
+ `preview phase root quota would exceed ${maxRootBytes} bytes`
197
+ );
198
+ }
199
+ }
200
+
201
+ function readRegularJSON(filePath, description) {
202
+ const stat = fileSystem.lstatSync(filePath);
203
+ if (!stat.isFile() || stat.isSymbolicLink()) {
204
+ throw new Error(`unsafe preview phase outbox path: ${filePath}`);
205
+ }
206
+ const value = JSON.parse(fileSystem.readFileSync(filePath, 'utf8'));
207
+ if (!value || typeof value !== 'object') {
208
+ throw new Error(`invalid preview phase ${description}: ${filePath}`);
209
+ }
210
+ return value;
211
+ }
212
+
213
+ function syncRunDirectory() {
214
+ const flags =
215
+ fs.constants.O_RDONLY |
216
+ (fs.constants.O_DIRECTORY || 0) |
217
+ (fs.constants.O_NOFOLLOW || 0);
218
+ const fd = fileSystem.openSync(runDir, flags);
219
+ try {
220
+ fileSystem.fsyncSync(fd);
221
+ } finally {
222
+ fileSystem.closeSync(fd);
223
+ }
224
+ }
225
+
226
+ function writeAll(fd, payload) {
227
+ let offset = 0;
228
+ while (offset < payload.length) {
229
+ const written = fileSystem.writeSync(
230
+ fd,
231
+ payload,
232
+ offset,
233
+ payload.length - offset
234
+ );
235
+ if (!Number.isInteger(written) || written <= 0) {
236
+ throw new Error('preview phase state write made no progress');
237
+ }
238
+ offset += written;
239
+ }
240
+ }
241
+
242
+ function publishImmutableState(filePath, label, value) {
243
+ const tempPath = path.join(
244
+ runDir,
245
+ `.${label}.${process.pid}.${writerInstanceID}.tmp`
246
+ );
247
+ let fd = null;
248
+ try {
249
+ const flags =
250
+ fs.constants.O_CREAT |
251
+ fs.constants.O_EXCL |
252
+ fs.constants.O_WRONLY |
253
+ (fs.constants.O_NOFOLLOW || 0);
254
+ fd = fileSystem.openSync(tempPath, flags, 0o600);
255
+ writeAll(fd, Buffer.from(`${JSON.stringify(value)}\n`, 'utf8'));
256
+ fileSystem.fsyncSync(fd);
257
+ fileSystem.closeSync(fd);
258
+ fd = null;
259
+ fileSystem.linkSync(tempPath, filePath);
260
+ syncRunDirectory();
261
+ } finally {
262
+ if (fd != null) {
263
+ try {
264
+ fileSystem.closeSync(fd);
265
+ } catch {}
266
+ }
267
+ try {
268
+ fileSystem.rmSync(tempPath, { force: true });
269
+ } catch {
270
+ // A stale temp file is harmless and never consumed by the reader.
271
+ }
272
+ }
273
+ }
274
+
275
+ function readAndValidateMetadata() {
276
+ const stat = fileSystem.lstatSync(metaPath);
277
+ if (!stat.isFile() || stat.isSymbolicLink()) {
278
+ throw new Error(`unsafe preview phase outbox path: ${metaPath}`);
279
+ }
280
+ fileSystem.chmodSync(metaPath, 0o600);
281
+ const metadata = JSON.parse(fileSystem.readFileSync(metaPath, 'utf8'));
282
+ if (
283
+ metadata.protocol !== 'preview-phase-outbox-v1' ||
284
+ metadata.schema_version !== 1 ||
285
+ metadata.run_id !== options.run_id ||
286
+ !/^[A-Za-z0-9_-]{1,128}$/.test(metadata.generation || '') ||
287
+ !Number.isFinite(metadata.created_at_ms) ||
288
+ metadata.created_at_ms <= 0 ||
289
+ (metadata.quiesce_supported !== undefined &&
290
+ metadata.quiesce_supported !== true)
291
+ ) {
292
+ throw new Error(`invalid preview phase outbox metadata: ${metaPath}`);
293
+ }
294
+ return metadata;
295
+ }
296
+
297
+ function ensureMetadata() {
298
+ if (metadata) return;
299
+ if (fileSystem.existsSync(metaPath)) {
300
+ metadata = readAndValidateMetadata();
301
+ return;
302
+ }
303
+ const candidate = {
304
+ protocol: 'preview-phase-outbox-v1',
305
+ schema_version: 1,
306
+ generation: randomUUID(),
307
+ run_id: options.run_id,
308
+ created_at_ms: now(),
309
+ quiesce_supported: true,
310
+ };
311
+ const tempPath = path.join(
312
+ runDir,
313
+ `.meta.${process.pid}.${candidate.generation}.tmp`
314
+ );
315
+ try {
316
+ fileSystem.writeFileSync(tempPath, `${JSON.stringify(candidate)}\n`, {
317
+ encoding: 'utf8',
318
+ flag: 'wx',
319
+ mode: 0o600,
320
+ });
321
+ try {
322
+ fileSystem.linkSync(tempPath, metaPath);
323
+ } catch (error) {
324
+ if (error?.code !== 'EEXIST') throw error;
325
+ }
326
+ } finally {
327
+ try {
328
+ fileSystem.rmSync(tempPath, { force: true });
329
+ } catch {
330
+ // A stale temp file is harmless and never consumed by the reader.
331
+ }
332
+ }
333
+ metadata = readAndValidateMetadata();
334
+ }
335
+
336
+ function validateProcessIdentityState(state, description) {
337
+ if (
338
+ !Number.isSafeInteger(state.pid) ||
339
+ state.pid <= 0 ||
340
+ (state.process_start_ticks !== undefined &&
341
+ !PROCESS_START_TICKS_PATTERN.test(state.process_start_ticks))
342
+ ) {
343
+ throw new Error(`invalid preview phase ${description}`);
344
+ }
345
+ }
346
+
347
+ function validateWriterLock(lock) {
348
+ assertStateGeneration(lock, 'writer lock');
349
+ validateProcessIdentityState(lock, 'writer lock');
350
+ if (
351
+ lock.protocol !== 'preview-phase-writer-v1' ||
352
+ lock.schema_version !== 1 ||
353
+ !STATE_ID_PATTERN.test(lock.writer_instance_id || '') ||
354
+ !Number.isFinite(lock.created_at_ms) ||
355
+ lock.created_at_ms <= 0
356
+ ) {
357
+ throw new Error(`invalid preview phase writer lock: ${writerLockPath}`);
358
+ }
359
+ return lock;
360
+ }
361
+
362
+ function writerBusyError() {
363
+ const busy = new Error(
364
+ `preview phase generation already has an active writer: ${options.run_id}`
365
+ );
366
+ busy.code = 'PREVIEW_PHASE_WRITER_BUSY';
367
+ return busy;
368
+ }
369
+
370
+ function acquireWriterLock() {
371
+ if (writerLockOwned) return;
372
+ const lock = {
373
+ protocol: 'preview-phase-writer-v1',
374
+ schema_version: 1,
375
+ run_id: options.run_id,
376
+ generation: metadata.generation,
377
+ writer_instance_id: writerInstanceID,
378
+ pid: process.pid,
379
+ ...(currentProcessStartTicks == null
380
+ ? {}
381
+ : { process_start_ticks: currentProcessStartTicks }),
382
+ created_at_ms: now(),
383
+ };
384
+ const tempPath = path.join(
385
+ runDir,
386
+ `.writer.${process.pid}.${writerInstanceID}.tmp`
387
+ );
388
+ try {
389
+ fileSystem.writeFileSync(tempPath, `${JSON.stringify(lock)}\n`, {
390
+ encoding: 'utf8',
391
+ flag: 'wx',
392
+ mode: 0o600,
393
+ });
394
+ try {
395
+ fileSystem.linkSync(tempPath, writerLockPath);
396
+ writerLockOwned = true;
397
+ syncRunDirectory();
398
+ } catch (error) {
399
+ if (error?.code !== 'EEXIST') throw error;
400
+ // Producers never delete or replace a lock they did not publish. Only
401
+ // the shell-server can prove owner death and finalize the generation;
402
+ // this removes path-ABA races between concurrent JS successors.
403
+ validateWriterLock(readRegularJSON(writerLockPath, 'writer lock'));
404
+ throw writerBusyError();
405
+ }
406
+ } finally {
407
+ try {
408
+ fileSystem.rmSync(tempPath, { force: true });
409
+ } catch {
410
+ // A stale temp file is harmless and never consumed by the reader.
411
+ }
412
+ }
413
+ }
414
+
415
+ function assertStateGeneration(state, description) {
416
+ if (
417
+ state.run_id !== options.run_id ||
418
+ state.generation !== metadata.generation
419
+ ) {
420
+ const error = new Error(
421
+ `preview phase ${description} generation mismatch: ${options.run_id}`
422
+ );
423
+ error.code = 'PREVIEW_PHASE_GENERATION_MISMATCH';
424
+ throw error;
425
+ }
426
+ }
427
+
428
+ function assertGenerationIsOpen() {
429
+ if (!fileSystem.existsSync(sealPath)) return;
430
+ const seal = readAndValidateSeal();
431
+ const error = new Error(
432
+ `preview phase sealed generation cannot be reopened: ${options.run_id}`
433
+ );
434
+ error.code = 'PREVIEW_PHASE_GENERATION_SEALED';
435
+ throw error;
436
+ }
437
+
438
+ function readAndValidateSeal() {
439
+ const seal = readRegularJSON(sealPath, 'seal');
440
+ assertStateGeneration(seal, 'seal');
441
+ if (
442
+ seal.protocol !== 'preview-phase-seal-v1' ||
443
+ seal.schema_version !== 1 ||
444
+ !Number.isSafeInteger(seal.final_size_bytes) ||
445
+ seal.final_size_bytes < 0 ||
446
+ !Number.isFinite(seal.sealed_at_ms) ||
447
+ seal.sealed_at_ms <= 0 ||
448
+ (seal.fallback_required !== undefined &&
449
+ typeof seal.fallback_required !== 'boolean')
450
+ ) {
451
+ throw new Error(`invalid preview phase seal: ${sealPath}`);
452
+ }
453
+ return seal;
454
+ }
455
+
456
+ function observeDegradedMarker() {
457
+ if (!fileSystem.existsSync(degradedPath)) return false;
458
+ const marker = readRegularJSON(degradedPath, 'degraded marker');
459
+ assertStateGeneration(marker, 'degraded marker');
460
+ if (
461
+ marker.protocol !== 'preview-phase-degraded-v1' ||
462
+ marker.schema_version !== 1 ||
463
+ typeof marker.first_failed_event_id !== 'string' ||
464
+ !Number.isFinite(marker.degraded_at_ms) ||
465
+ marker.degraded_at_ms <= 0 ||
466
+ !STATE_ID_PATTERN.test(marker.reason || '')
467
+ ) {
468
+ throw new Error(`invalid preview phase degraded marker: ${degradedPath}`);
469
+ }
470
+ degraded = true;
471
+ fallbackRequired = true;
472
+ return true;
473
+ }
474
+
475
+ function assertGenerationIsNotDegraded() {
476
+ if (!observeDegradedMarker()) return;
477
+ throw degradedError();
478
+ }
479
+
480
+ function removeOwnedWriterLock() {
481
+ const lock = readRegularJSON(writerLockPath, 'writer lock');
482
+ if (
483
+ lock.protocol !== 'preview-phase-writer-v1' ||
484
+ lock.schema_version !== 1 ||
485
+ lock.run_id !== options.run_id ||
486
+ lock.generation !== metadata.generation ||
487
+ lock.writer_instance_id !== writerInstanceID
488
+ ) {
489
+ throw new Error('preview phase writer lock ownership changed');
490
+ }
491
+ fileSystem.rmSync(writerLockPath);
492
+ writerLockOwned = false;
493
+ syncRunDirectory();
494
+ }
495
+
496
+ function recoverTornTail() {
497
+ if (recovered) return;
498
+ if (!fileSystem.existsSync(eventsPath)) {
499
+ recovered = true;
500
+ return;
501
+ }
502
+ const readFlags = fs.constants.O_RDWR | (fs.constants.O_NOFOLLOW || 0);
503
+ const fd = fileSystem.openSync(eventsPath, readFlags);
504
+ try {
505
+ const size = fileSystem.fstatSync(fd).size;
506
+ if (size === 0) {
507
+ recovered = true;
508
+ return;
509
+ }
510
+ const tailByte = Buffer.allocUnsafe(1);
511
+ fileSystem.readSync(fd, tailByte, 0, 1, size - 1);
512
+ if (tailByte[0] === 0x0a) {
513
+ recovered = true;
514
+ return;
515
+ }
516
+
517
+ const chunk = Buffer.allocUnsafe(64 * 1024);
518
+ let scanEnd = size;
519
+ while (scanEnd > 0) {
520
+ const scanStart = Math.max(0, scanEnd - chunk.length);
521
+ const length = scanEnd - scanStart;
522
+ const bytesRead = fileSystem.readSync(fd, chunk, 0, length, scanStart);
523
+ const lastNewline = chunk.lastIndexOf(0x0a, bytesRead - 1);
524
+ if (lastNewline >= 0) {
525
+ fileSystem.ftruncateSync(fd, scanStart + lastNewline + 1);
526
+ recovered = true;
527
+ return;
528
+ }
529
+ scanEnd = scanStart;
530
+ }
531
+ fileSystem.ftruncateSync(fd, 0);
532
+ recovered = true;
533
+ } finally {
534
+ fileSystem.closeSync(fd);
535
+ }
536
+ }
537
+
538
+ function degradedError() {
539
+ const error = new Error('preview phase outbox is degraded');
540
+ error.code = 'PREVIEW_PHASE_OUTBOX_DEGRADED';
541
+ return error;
542
+ }
543
+
544
+ function latchDegraded(event, cause) {
545
+ fallbackRequired = true;
546
+ if (degraded) return;
547
+ degraded = true;
548
+ if (!metadata) return;
549
+ const reason =
550
+ typeof cause?.code === 'string' && STATE_ID_PATTERN.test(cause.code)
551
+ ? cause.code
552
+ : 'append_failed';
553
+ try {
554
+ publishImmutableState(degradedPath, 'degraded', {
555
+ protocol: 'preview-phase-degraded-v1',
556
+ schema_version: 1,
557
+ run_id: options.run_id,
558
+ generation: metadata.generation,
559
+ first_failed_event_id:
560
+ typeof event?.event_id === 'string' ? event.event_id : '',
561
+ degraded_at_ms: now(),
562
+ reason,
563
+ });
564
+ } catch {
565
+ // A total filesystem failure cannot be made durable. The in-memory latch
566
+ // still prevents this writer from creating holes in the JSONL prefix.
567
+ }
568
+ }
569
+
570
+ function append(event) {
571
+ if (closed) throw new Error('preview phase outbox is closed');
572
+ if (degraded) throw degradedError();
573
+ const payload = JSON.stringify(event);
574
+ if (Buffer.byteLength(payload, 'utf8') > MAX_EVENT_BYTES) {
575
+ const error = new Error(
576
+ `preview phase event exceeds ${MAX_EVENT_BYTES} bytes`
577
+ );
578
+ error.code = 'PREVIEW_PHASE_EVENT_TOO_LARGE';
579
+ throw error;
580
+ }
581
+ const record = Buffer.from(`${payload}\n`, 'utf8');
582
+ try {
583
+ assertSafeDirectory(fileSystem, root);
584
+ assertRootDirectoryQuota();
585
+ assertSafeDirectory(fileSystem, runDir);
586
+ ensureMetadata();
587
+ assertGenerationIsOpen();
588
+ acquireWriterLock();
589
+ // Any existing writer identity is authoritative to JS. Dead-owner
590
+ // recovery and torn-tail finalization belong exclusively to shell-server.
591
+ assertGenerationIsNotDegraded();
592
+ assertRootByteQuota(record.length);
593
+ if (eventsFD == null) {
594
+ if (fileSystem.existsSync(eventsPath)) {
595
+ const stat = fileSystem.lstatSync(eventsPath);
596
+ if (!stat.isFile() || stat.isSymbolicLink()) {
597
+ throw new Error(`unsafe preview phase outbox path: ${eventsPath}`);
598
+ }
599
+ }
600
+ recoverTornTail();
601
+ const flags =
602
+ fs.constants.O_CREAT |
603
+ fs.constants.O_WRONLY |
604
+ fs.constants.O_APPEND |
605
+ (fs.constants.O_NOFOLLOW || 0);
606
+ eventsFD = fileSystem.openSync(eventsPath, flags, 0o600);
607
+ fileSystem.fchmodSync(eventsFD, 0o600);
608
+ committedBytes = fileSystem.fstatSync(eventsFD).size;
609
+ }
610
+ if (committedBytes + record.length > maxRunBytes) {
611
+ const error = new Error(
612
+ `preview phase run quota would exceed ${maxRunBytes} bytes`
613
+ );
614
+ error.code = 'PREVIEW_PHASE_RUN_QUOTA_EXCEEDED';
615
+ throw error;
616
+ }
617
+ let offset = 0;
618
+ while (offset < record.length) {
619
+ const written = fileSystem.writeSync(
620
+ eventsFD,
621
+ record,
622
+ offset,
623
+ record.length - offset
624
+ );
625
+ if (!Number.isInteger(written) || written <= 0) {
626
+ throw new Error('preview phase outbox write made no progress');
627
+ }
628
+ offset += written;
629
+ }
630
+ committedBytes += record.length;
631
+ return true;
632
+ } catch (error) {
633
+ if (eventsFD != null) {
634
+ try {
635
+ fileSystem.closeSync(eventsFD);
636
+ } catch {}
637
+ eventsFD = null;
638
+ committedBytes = null;
639
+ recovered = false;
640
+ }
641
+ latchDegraded(event, error);
642
+ throw error;
643
+ }
644
+ }
645
+
646
+ function flush() {
647
+ if (eventsFD == null) return true;
648
+ try {
649
+ fileSystem.fdatasyncSync(eventsFD);
650
+ return true;
651
+ } catch (error) {
652
+ fallbackRequired = true;
653
+ throw error;
654
+ }
655
+ }
656
+
657
+ function pollQuiesce() {
658
+ if (closed) return closeResult;
659
+ if (sealRequested) return close();
660
+ // A reporter that never acquired this generation's writer lock must not
661
+ // acknowledge or act on a request intended for the actual owner.
662
+ if (!metadata || !writerLockOwned) return false;
663
+ if (!fileSystem.existsSync(quiesceRequestPath)) return false;
664
+ const request = readRegularJSON(quiesceRequestPath, 'quiesce request');
665
+ assertStateGeneration(request, 'quiesce request');
666
+ if (
667
+ request.protocol !== 'preview-phase-quiesce-v1' ||
668
+ request.schema_version !== 1 ||
669
+ !STATE_ID_PATTERN.test(request.request_id || '') ||
670
+ !Number.isFinite(request.requested_at_ms) ||
671
+ request.requested_at_ms <= 0
672
+ ) {
673
+ throw new Error(
674
+ `invalid preview phase quiesce request: ${quiesceRequestPath}`
675
+ );
676
+ }
677
+ return close();
678
+ }
679
+
680
+ function close() {
681
+ if (closed) return closeResult;
682
+ sealRequested = true;
683
+ let finalSize = null;
684
+ try {
685
+ // A reporter that never attempted persistence has no generation to
686
+ // close. Persistence failures set degraded/fallbackRequired and retry
687
+ // this initialization when the filesystem becomes available again.
688
+ if (!metadata && !degraded && !fallbackRequired) {
689
+ closed = true;
690
+ closeResult = true;
691
+ return true;
692
+ }
693
+ assertSafeDirectory(fileSystem, root);
694
+ assertRootDirectoryQuota();
695
+ assertSafeDirectory(fileSystem, runDir);
696
+ ensureMetadata();
697
+
698
+ if (fileSystem.existsSync(sealPath)) {
699
+ const seal = readAndValidateSeal();
700
+ fallbackRequired ||= seal.fallback_required === true;
701
+ if (writerLockOwned) {
702
+ removeOwnedWriterLock();
703
+ } else if (fileSystem.existsSync(writerLockPath)) {
704
+ closeResult = false;
705
+ return false;
706
+ }
707
+ closed = true;
708
+ closeResult = true;
709
+ return true;
710
+ }
711
+
712
+ if (!writerLockOwned) {
713
+ acquireWriterLock();
714
+ }
715
+ observeDegradedMarker();
716
+ if (eventsFD != null) {
717
+ fileSystem.fdatasyncSync(eventsFD);
718
+ finalSize = fileSystem.fstatSync(eventsFD).size;
719
+ fileSystem.closeSync(eventsFD);
720
+ eventsFD = null;
721
+ committedBytes = null;
722
+ } else {
723
+ recoverTornTail();
724
+ // The first append can fail after the writer lock is acquired but
725
+ // before events.jsonl is created (for example, aggregate quota). Such
726
+ // a degraded generation still needs an empty, sealed prefix so the
727
+ // lock is releasable and the reader can merge stdout_fallback safely.
728
+ const flags =
729
+ fs.constants.O_CREAT |
730
+ fs.constants.O_RDWR |
731
+ (fs.constants.O_NOFOLLOW || 0);
732
+ const fd = fileSystem.openSync(eventsPath, flags, 0o600);
733
+ try {
734
+ fileSystem.fchmodSync(fd, 0o600);
735
+ fileSystem.fdatasyncSync(fd);
736
+ finalSize = fileSystem.fstatSync(fd).size;
737
+ } finally {
738
+ fileSystem.closeSync(fd);
739
+ }
740
+ }
741
+ publishImmutableState(sealPath, 'sealed', {
742
+ protocol: 'preview-phase-seal-v1',
743
+ schema_version: 1,
744
+ run_id: options.run_id,
745
+ generation: metadata.generation,
746
+ final_size_bytes: finalSize,
747
+ sealed_at_ms: now(),
748
+ fallback_required: fallbackRequired,
749
+ });
750
+ removeOwnedWriterLock();
751
+ closeResult = true;
752
+ closed = true;
753
+ } catch {
754
+ closeResult = false;
755
+ fallbackRequired = true;
756
+ if (eventsFD != null) {
757
+ try {
758
+ fileSystem.closeSync(eventsFD);
759
+ } catch {}
760
+ eventsFD = null;
761
+ committedBytes = null;
762
+ }
763
+ // Keep the unique writer owner and pending close intent for transient
764
+ // retries. If the process dies, shell-server is the sole authority that
765
+ // verifies PID+starttime, repairs the tail, and seals the generation.
766
+ }
767
+ return closeResult;
768
+ }
769
+
770
+ return {
771
+ append,
772
+ flush,
773
+ close,
774
+ poll_quiesce: pollQuiesce,
775
+ events_path: eventsPath,
776
+ meta_path: metaPath,
777
+ seal_path: sealPath,
778
+ degraded_path: degradedPath,
779
+ quiesce_request_path: quiesceRequestPath,
780
+ };
781
+ }
782
+
783
+ module.exports = {
784
+ DEFAULT_OUTBOX_ROOT,
785
+ DEFAULT_MAX_RUN_BYTES,
786
+ DEFAULT_MAX_ROOT_BYTES,
787
+ DEFAULT_MAX_ROOT_DIRECTORIES,
788
+ DEFAULT_MAX_ROOT_SCAN_DIRECTORIES,
789
+ MAX_EVENT_BYTES,
790
+ createPreviewPhaseOutbox,
791
+ };
@@ -1,11 +1,13 @@
1
1
  'use strict';
2
2
 
3
3
  const fs = require('fs');
4
+ const crypto = require('crypto');
4
5
  const net = require('net');
5
6
  const path = require('path');
7
+ const { createPreviewPhaseOutbox } = require('./preview-phase-outbox.cjs');
6
8
 
7
9
  const PREFIX = '[MiaodaPreviewPhase] ';
8
- const RUN_ID_PATTERN = /^prv_[A-Za-z0-9_-]{1,128}$/;
10
+ const RUN_ID_PATTERN = /^prv_[A-Za-z0-9_-]{1,60}$/;
9
11
 
10
12
  function resolveAppId(env) {
11
13
  const basePath = env.CLIENT_BASE_PATH || '';
@@ -13,30 +15,340 @@ function resolveAppId(env) {
13
15
  return match ? match[1] : env.MIAODA_APP_ID || '';
14
16
  }
15
17
 
18
+ function createPreviewPhaseFrameDecoder(options = {}) {
19
+ const maxBytes = options.max_bytes || 16 * 1024;
20
+ const onFrame = options.on_frame;
21
+ const onOversizedFrame = options.on_oversized_frame || (() => {});
22
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
23
+ throw new Error('preview phase frame max_bytes must be a positive integer');
24
+ }
25
+ if (typeof onFrame !== 'function') {
26
+ throw new Error('preview phase frame on_frame callback is required');
27
+ }
28
+
29
+ let pending = [];
30
+ let pendingBytes = 0;
31
+ let droppingOversizedFrame = false;
32
+
33
+ function reset() {
34
+ pending = [];
35
+ pendingBytes = 0;
36
+ }
37
+
38
+ function push(chunk) {
39
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
40
+ let offset = 0;
41
+ while (offset < buffer.length) {
42
+ const newline = buffer.indexOf(0x0a, offset);
43
+ const end = newline < 0 ? buffer.length : newline;
44
+ const segment = buffer.subarray(offset, end);
45
+
46
+ if (!droppingOversizedFrame) {
47
+ if (pendingBytes + segment.length > maxBytes) {
48
+ reset();
49
+ droppingOversizedFrame = true;
50
+ onOversizedFrame();
51
+ } else if (segment.length > 0) {
52
+ pending.push(segment);
53
+ pendingBytes += segment.length;
54
+ }
55
+ }
56
+
57
+ if (newline < 0) return;
58
+ if (!droppingOversizedFrame) {
59
+ onFrame(Buffer.concat(pending, pendingBytes).toString('utf8'));
60
+ }
61
+ reset();
62
+ droppingOversizedFrame = false;
63
+ offset = newline + 1;
64
+ }
65
+ }
66
+
67
+ return { push };
68
+ }
69
+
16
70
  function createPreviewPhaseReporter(options = {}) {
17
71
  const env = options.env || process.env;
18
72
  const write = options.write || (line => process.stdout.write(`${line}\n`));
19
73
  const now = options.now || Date.now;
74
+ const randomUUID = options.random_uuid || crypto.randomUUID;
75
+ const producer = options.producer || 'dev_orchestrator';
76
+ const onDiagnostic =
77
+ options.on_diagnostic ||
78
+ (diagnostic => {
79
+ try {
80
+ process.stderr.write(
81
+ `[MiaodaPreviewOutbox] ${JSON.stringify(diagnostic)}\n`
82
+ );
83
+ } catch {
84
+ // Diagnostics must never block preview startup.
85
+ }
86
+ });
87
+ const producerInstanceID = randomUUID();
88
+ const runId = env.MIAODA_PREVIEW_RUN_ID || '';
89
+ const outbox = RUN_ID_PATTERN.test(runId)
90
+ ? createPreviewPhaseOutbox({
91
+ root: options.outbox_root || env.MIAODA_PREVIEW_PHASE_OUTBOX_ROOT,
92
+ run_id: runId,
93
+ fs: options.fs,
94
+ random_uuid: randomUUID,
95
+ now,
96
+ })
97
+ : null;
98
+ const producerSequences = new Map();
99
+ const startupTerminalPhases = new Set();
100
+ let reporterSealed = false;
101
+ let quiescePollTimer = null;
102
+
103
+ function reportDiagnostic(diagnostic) {
104
+ try {
105
+ onDiagnostic(diagnostic);
106
+ } catch {
107
+ // Diagnostics must never become a preview-startup failure mode.
108
+ }
109
+ }
20
110
 
21
- function emit(phase, status, detail = {}) {
22
- const runId = env.MIAODA_PREVIEW_RUN_ID || '';
23
- if (!RUN_ID_PATTERN.test(runId)) return false;
111
+ function emitForProducer(eventProducer, phase, status, detail = {}) {
112
+ if (!RUN_ID_PATTERN.test(runId) || reporterSealed) return false;
24
113
  const atMs = detail.at_ms == null ? now() : detail.at_ms;
114
+ const producerSeq = (producerSequences.get(eventProducer) || 0) + 1;
115
+ producerSequences.set(eventProducer, producerSeq);
25
116
  const event = {
26
117
  ...detail,
27
118
  schema_version: 1,
119
+ event_id: `${runId}:${randomUUID()}`,
28
120
  run_id: runId,
29
121
  app_id: resolveAppId(env),
30
122
  sandbox_id: env.SANDBOX_ID || '',
31
123
  phase,
32
124
  status,
33
125
  at_ms: atMs,
126
+ producer: eventProducer,
127
+ producer_instance_id:
128
+ eventProducer === producer
129
+ ? producerInstanceID
130
+ : `${producerInstanceID}:${eventProducer}`,
131
+ producer_seq: producerSeq,
132
+ recorded_at_ms: now(),
133
+ transport: 'outbox',
34
134
  };
35
- write(`${PREFIX}${JSON.stringify(event)}`);
36
- return true;
135
+ let persisted = false;
136
+ try {
137
+ persisted = outbox.append(event);
138
+ } catch (error) {
139
+ if (error?.code === 'PREVIEW_PHASE_EVENT_TOO_LARGE') {
140
+ reportDiagnostic({
141
+ code: 'event_rejected',
142
+ reason: 'event_too_large',
143
+ run_id: runId,
144
+ phase,
145
+ event_id: event.event_id,
146
+ });
147
+ return false;
148
+ }
149
+ event.transport = 'stdout_fallback';
150
+ if (error?.code !== 'PREVIEW_PHASE_OUTBOX_DEGRADED') {
151
+ reportDiagnostic({
152
+ code: 'outbox_append_failed',
153
+ run_id: runId,
154
+ phase,
155
+ event_id: event.event_id,
156
+ error: error instanceof Error ? error.message : String(error),
157
+ });
158
+ }
159
+ }
160
+ let mirrored = false;
161
+ try {
162
+ write(`${PREFIX}${JSON.stringify(event)}`);
163
+ mirrored = true;
164
+ } catch (error) {
165
+ reportDiagnostic({
166
+ code: 'stdout_mirror_failed',
167
+ run_id: runId,
168
+ phase,
169
+ event_id: event.event_id,
170
+ error: error instanceof Error ? error.message : String(error),
171
+ });
172
+ }
173
+ const delivered = persisted || mirrored;
174
+ if (
175
+ delivered &&
176
+ status !== 'start' &&
177
+ (phase === 'backend_tcp_ready' || phase === 'vite_http_listening')
178
+ ) {
179
+ startupTerminalPhases.add(phase);
180
+ if (startupTerminalPhases.size === 2) {
181
+ flush();
182
+ close();
183
+ }
184
+ }
185
+ return delivered;
186
+ }
187
+
188
+ function emit(phase, status, detail = {}) {
189
+ return emitForProducer(producer, phase, status, detail);
190
+ }
191
+
192
+ function ingest(rawFrame, context = {}) {
193
+ if (
194
+ typeof rawFrame !== 'string' ||
195
+ Buffer.byteLength(rawFrame, 'utf8') > 16 * 1024
196
+ ) {
197
+ return false;
198
+ }
199
+ const isLegacyStdoutFrame =
200
+ context.allow_legacy_stdout === true && rawFrame.startsWith(PREFIX);
201
+ const serializedFrame = isLegacyStdoutFrame
202
+ ? rawFrame.slice(PREFIX.length).trim()
203
+ : rawFrame;
204
+ let frame;
205
+ try {
206
+ frame = JSON.parse(serializedFrame);
207
+ } catch {
208
+ return false;
209
+ }
210
+ const allowedPhases = Array.isArray(context.allowed_phases)
211
+ ? context.allowed_phases
212
+ : [];
213
+ if (
214
+ (!isLegacyStdoutFrame &&
215
+ frame?.protocol !== 'preview_phase_producer_v1') ||
216
+ frame.schema_version !== 1 ||
217
+ frame.run_id !== runId ||
218
+ !allowedPhases.includes(frame.phase) ||
219
+ !/^(?:start|success|error|fallback|skipped|cancelled)$/.test(
220
+ frame.status || ''
221
+ ) ||
222
+ !Number.isFinite(frame.at_ms) ||
223
+ frame.at_ms <= 0
224
+ ) {
225
+ return false;
226
+ }
227
+ const detail = {
228
+ at_ms: frame.at_ms,
229
+ ...(Number.isFinite(frame.duration_ms) && frame.duration_ms >= 0
230
+ ? { duration_ms: frame.duration_ms }
231
+ : {}),
232
+ ...(Number.isFinite(frame.precision_ms) && frame.precision_ms >= 0
233
+ ? { precision_ms: frame.precision_ms }
234
+ : {}),
235
+ ...(typeof frame.exact === 'boolean' ? { exact: frame.exact } : {}),
236
+ ...(Number.isSafeInteger(context.attempt) && context.attempt > 0
237
+ ? { attempt: context.attempt }
238
+ : {}),
239
+ };
240
+ return emitForProducer(
241
+ context.producer || 'child_process',
242
+ frame.phase,
243
+ frame.status,
244
+ detail
245
+ );
246
+ }
247
+
248
+ function flush() {
249
+ if (reporterSealed) return true;
250
+ if (!outbox) return true;
251
+ try {
252
+ return outbox.flush();
253
+ } catch (error) {
254
+ reportDiagnostic({
255
+ code: 'outbox_flush_failed',
256
+ run_id: runId,
257
+ error: error instanceof Error ? error.message : String(error),
258
+ });
259
+ return false;
260
+ }
37
261
  }
38
262
 
39
- return { emit };
263
+ function stopQuiescePolling() {
264
+ if (quiescePollTimer == null) return;
265
+ clearInterval(quiescePollTimer);
266
+ quiescePollTimer = null;
267
+ }
268
+
269
+ function pollQuiesce() {
270
+ if (reporterSealed) return true;
271
+ if (!outbox) return false;
272
+ try {
273
+ const sealed = outbox.poll_quiesce();
274
+ if (sealed) {
275
+ reporterSealed = true;
276
+ stopQuiescePolling();
277
+ }
278
+ return sealed;
279
+ } catch (error) {
280
+ reportDiagnostic({
281
+ code: 'outbox_quiesce_failed',
282
+ run_id: runId,
283
+ error: error instanceof Error ? error.message : String(error),
284
+ });
285
+ return false;
286
+ }
287
+ }
288
+
289
+ function close() {
290
+ if (reporterSealed) return true;
291
+ if (!outbox) return true;
292
+ try {
293
+ const sealed = outbox.close();
294
+ if (sealed) {
295
+ reporterSealed = true;
296
+ stopQuiescePolling();
297
+ }
298
+ return sealed;
299
+ } catch (error) {
300
+ reportDiagnostic({
301
+ code: 'outbox_close_failed',
302
+ run_id: runId,
303
+ error: error instanceof Error ? error.message : String(error),
304
+ });
305
+ return false;
306
+ }
307
+ }
308
+
309
+ if (outbox) {
310
+ const intervalMs = options.quiesce_poll_interval_ms ?? 100;
311
+ if (Number.isSafeInteger(intervalMs) && intervalMs > 0) {
312
+ quiescePollTimer = setInterval(pollQuiesce, intervalMs);
313
+ if (typeof quiescePollTimer.unref === 'function') {
314
+ quiescePollTimer.unref();
315
+ }
316
+ }
317
+ }
318
+
319
+ return { emit, ingest, flush, close, pollQuiesce };
320
+ }
321
+
322
+ async function closePreviewPhaseReporterWithRetry(reporter, options = {}) {
323
+ const retryDelaysMs = options.retry_delays_ms || [0, 25, 100, 250, 500, 1000];
324
+ const sleep =
325
+ options.sleep ||
326
+ (delayMs => new Promise(resolve => setTimeout(resolve, delayMs)));
327
+ if (
328
+ !reporter ||
329
+ typeof reporter.flush !== 'function' ||
330
+ typeof reporter.close !== 'function' ||
331
+ !Array.isArray(retryDelaysMs) ||
332
+ retryDelaysMs.length === 0 ||
333
+ retryDelaysMs.some(delayMs => !Number.isSafeInteger(delayMs) || delayMs < 0)
334
+ ) {
335
+ throw new Error('invalid preview phase reporter close retry configuration');
336
+ }
337
+
338
+ for (const delayMs of retryDelaysMs) {
339
+ if (delayMs > 0) await sleep(delayMs);
340
+ try {
341
+ reporter.flush();
342
+ } catch {
343
+ // close() repeats the durable flush and reports the authoritative result.
344
+ }
345
+ try {
346
+ if (reporter.close()) return true;
347
+ } catch {
348
+ // A later bounded attempt may succeed after a transient filesystem fault.
349
+ }
350
+ }
351
+ return false;
40
352
  }
41
353
 
42
354
  function createTscServerCommand(reason, missingModules = []) {
@@ -449,7 +761,9 @@ async function waitForTcpReady(options) {
449
761
  module.exports = {
450
762
  applyPreviewArtifactHostOverrides,
451
763
  buildListeningPortLookupArgs,
764
+ closePreviewPhaseReporterWithRetry,
452
765
  createDeferredTypecheckGate,
766
+ createPreviewPhaseFrameDecoder,
453
767
  createPreviewPhaseReporter,
454
768
  normalizeTcpPort,
455
769
  parseTscWatchSummary,