@opengeni/runtime 0.11.0 → 0.13.3

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.
@@ -18,6 +18,7 @@
18
18
  // gVisor harness (V2 PASSED live on Modal: XTEST input read-back under runsc).
19
19
 
20
20
  import { DESKTOP_STREAM_PORT } from "@opengeni/contracts";
21
+ import { parseExecResponseBanner } from "./exec-banner";
21
22
 
22
23
  // Re-export under the canonical name the module spec uses (STREAM_PORT) while
23
24
  // keeping DESKTOP_STREAM_PORT as the single source of truth (contracts).
@@ -31,6 +32,20 @@ export const STREAM_PORT = DESKTOP_STREAM_PORT;
31
32
  // warm path AND the cold-box paint warm-up without masking a genuine wedge.
32
33
  export const DISPLAY_STACK_TIMEOUT_MS = 90_000;
33
34
 
35
+ // Provider exec APIs use `yieldTimeMs` as an output-yield window, not a process
36
+ // deadline. Poll often enough to surface stage telemetry while the command keeps
37
+ // running, but let the in-box `timeout(1)` own termination (see
38
+ // `boundedDisplayStackCommand`).
39
+ const DISPLAY_STACK_PROVIDER_YIELD_MS = 15_000;
40
+ const DISPLAY_STACK_PROVIDER_POLL_MS = 5_000;
41
+ const DISPLAY_STACK_TERMINATION_GRACE_MS = 5_000;
42
+ // Provider cancellation is transport-level and may never settle. Keep the
43
+ // JavaScript-side cleanup/drain bounded as well; a later caller either gets a
44
+ // positive in-box quiescence proof or remains fail-closed, but it must never
45
+ // wait forever on a provider promise.
46
+ const DISPLAY_STACK_CLEANUP_CALL_MS = 250;
47
+ const DISPLAY_STACK_DRAIN_GRACE_MS = 100;
48
+
34
49
  // PAINTABLE-FRAME gate: poll scrot up to this many times, this many seconds apart,
35
50
  // waiting for an actually-PAINTED frame before declaring the stack "up" (~30s worst case).
36
51
  const PAINT_PROBE_ATTEMPTS = 150;
@@ -93,7 +108,7 @@ export const DEFAULT_DESKTOP_GEOMETRY: DesktopGeometry = { width: 1280, height:
93
108
  * value to viewers by the caller; this error is for diagnostics. */
94
109
  export class DisplayStackError extends Error {
95
110
  readonly exitCode: number;
96
- readonly stage: "xvfb" | "x11vnc" | "websockify" | "paint" | "unknown";
111
+ readonly stage: "xvfb" | "x11vnc" | "websockify" | "paint" | "lock" | "timeout" | "unknown";
97
112
 
98
113
  constructor(exitCode: number, output: string) {
99
114
  const stage =
@@ -105,7 +120,11 @@ export class DisplayStackError extends Error {
105
120
  ? "websockify"
106
121
  : exitCode === 14
107
122
  ? "paint"
108
- : "unknown";
123
+ : exitCode === 15
124
+ ? "lock"
125
+ : exitCode === 124 || /OPENGENI_DISPLAY_TIMEOUT/u.test(output)
126
+ ? "timeout"
127
+ : "unknown";
109
128
  super(
110
129
  `desktop display stack failed at stage "${stage}" (exit ${exitCode})${output ? `:\n${output}` : ""}`,
111
130
  );
@@ -132,26 +151,68 @@ type ExecResultLike = {
132
151
  stdout?: string;
133
152
  stderr?: string;
134
153
  exitCode?: number | null;
154
+ sessionId?: number;
155
+ session_id?: number;
135
156
  };
136
157
  type ExecCapableSession = {
158
+ state?: unknown;
137
159
  exec?: (args: {
138
160
  cmd: string;
139
161
  yieldTimeMs?: number;
140
162
  maxOutputTokens?: number;
163
+ signal?: AbortSignal;
141
164
  }) => Promise<ExecResultLike>;
142
165
  execCommand?: (args: {
143
166
  cmd: string;
144
167
  yieldTimeMs?: number;
145
168
  maxOutputTokens?: number;
169
+ signal?: AbortSignal;
146
170
  }) => Promise<string>;
171
+ writeStdin?: (args: {
172
+ sessionId: number;
173
+ chars: string;
174
+ yieldTimeMs?: number;
175
+ maxOutputTokens?: number;
176
+ signal?: AbortSignal;
177
+ }) => Promise<ExecResultLike | string>;
178
+ };
179
+
180
+ export type DisplayStackCallerKind = "viewer" | "computer" | "unknown";
181
+ export type DisplayStackClassification = "cold" | "already_ready" | "contention" | "unknown";
182
+ export type DisplayStackTelemetryStatus = "started" | "waiting" | "completed" | "failed";
183
+
184
+ /** Secret-free, bounded-cardinality stage observation. Provider/sandbox IDs are
185
+ * log correlation fields, never metric labels. */
186
+ export type DisplayStackTelemetryEvent = {
187
+ type: "display_stack.stage";
188
+ requestId: string;
189
+ stage: string;
190
+ status: DisplayStackTelemetryStatus;
191
+ elapsedMs: number;
192
+ source: "host" | "sandbox";
193
+ classification: DisplayStackClassification;
194
+ callerKind: DisplayStackCallerKind;
195
+ sandboxId?: string;
196
+ leaseEpoch?: number;
197
+ providerSessionId?: number;
198
+ };
199
+
200
+ export type DisplayStackTelemetryContext = {
201
+ callerKind: DisplayStackCallerKind;
202
+ sandboxId?: string;
203
+ leaseEpoch?: number;
147
204
  };
148
205
 
149
206
  export type EnsureDisplayStackOptions = {
150
207
  geometry?: DesktopGeometry;
151
208
  /** The exposed stream port; defaults to 6080. */
152
209
  port?: number;
153
- /** Per-exec timeout; defaults to DISPLAY_STACK_TIMEOUT_MS. */
210
+ /** Whole-operation wall-clock timeout; defaults to DISPLAY_STACK_TIMEOUT_MS. */
154
211
  timeoutMs?: number;
212
+ /** Caller correlation. Supplying it enables the default structured log sink. */
213
+ telemetryContext?: DisplayStackTelemetryContext;
214
+ /** Optional non-blocking telemetry sink (tests/embedders may replace logging). */
215
+ onTelemetry?: (event: DisplayStackTelemetryEvent) => void | Promise<void>;
155
216
  };
156
217
 
157
218
  export type EnsureDisplayStackResult = {
@@ -214,16 +275,30 @@ export function buildDisplayStackScript(options: EnsureDisplayStackOptions = {})
214
275
  // — an already-up display paints on the first probe). Lives in the runtime-built script
215
276
  // (not the baked image up-script) so it ships with the worker/api, no image rebuild —
216
277
  // and its size floor holds against the currently-deployed image too.
278
+ const lockedLauncher =
279
+ `ds_started=$1; fw=$2; ds_class=cold; ` +
280
+ `ds_stage() { ds_now=$(date +%s%3N); echo "OPENGENI_DISPLAY_STAGE stage=$1 elapsed_ms=$((ds_now-ds_started)) classification=$ds_class"; }; ` +
281
+ `fa=$(date +%s%3N); fd=$((fa-fw)); [ "$fd" -ge 50 ] && ds_class=contention; ` +
282
+ `ds_stage outer_flock_acquired; ds_stage launcher_issued; ` +
283
+ `env ${env} opengeni-desktop-up; urc=$?; ds_stage launcher_complete; exit "$urc"`;
217
284
  const bringUp =
218
285
  `if nc -z 127.0.0.1 ${port} >/dev/null 2>&1 && nc -z 127.0.0.1 5900 >/dev/null 2>&1; then ` +
286
+ `ds_class=already_ready; ds_stage precheck_ready; ` +
219
287
  `echo "OPENGENI_DESKTOP_UP port=${port} geometry=${geometry.width}x${geometry.height} dpi=${geometry.dpi} (precheck)"; ` +
220
288
  `else ` +
221
- `mkdir -p /tmp/opengeni-desktop && ` +
222
- `flock -w 45 /tmp/opengeni-desktop/up.outer.lock ` +
223
- `env ${env} opengeni-desktop-up; ` +
289
+ `ds_class=cold; ds_stage precheck_miss; ` +
290
+ `mkdir -p /tmp/opengeni-desktop; ` +
291
+ `ds_stage outer_flock_wait; fw=$(date +%s%3N); ` +
292
+ // Command-mode flock is the supervisor: it retains the lock until the launcher
293
+ // exits, while --close prevents Xvfb/XFCE/x11vnc/websockify from inheriting the
294
+ // lock descriptor when the launcher intentionally detaches them.
295
+ `flock --close --conflict-exit-code 75 --wait 45 /tmp/opengeni-desktop/up.outer.lock ` +
296
+ `bash -c ${shellQuote(lockedLauncher)} _ "$ds_started" "$fw"; frc=$?; ` +
297
+ `if [ "$frc" -eq 75 ]; then ds_stage outer_flock_timeout; echo "OPENGENI_DESKTOP_LOCK_TIMEOUT"; exit 15; fi; ` +
298
+ `if [ "$frc" -ne 0 ]; then exit "$frc"; fi; ` +
224
299
  `fi`;
225
300
  const paintProbe =
226
- `p=/tmp/opengeni-desktop/paint-probe.png; prev=0; ` +
301
+ `ds_stage paint_probe_started; p=/tmp/opengeni-desktop/paint-probe.png; prev=0; ` +
227
302
  `for i in $(seq 1 ${PAINT_PROBE_ATTEMPTS}); do ` +
228
303
  // Capture, then measure the PNG byte-size. `wc -c < "$p"` yields a bare integer; a
229
304
  // failed scrot leaves sz=0. A frame at/above PAINT_MIN_BYTES is a real painted desktop.
@@ -234,14 +309,18 @@ export function buildDisplayStackScript(options: EnsureDisplayStackOptions = {})
234
309
  // growing (the full desktop, panel + icons included, is up), not merely crossed the
235
310
  // floor mid-paint on a staged gVisor boot. ($sz/$prev/$d are bare shell — no ${}
236
311
  // braces — so JS leaves them for bash; ${PAINT_*} ARE JS constants and interpolate.)
237
- `if [ "$sz" -ge ${PAINT_MIN_BYTES} ] && [ "$prev" -ge ${PAINT_MIN_BYTES} ]; then d=$((sz-prev)); [ "$d" -lt 0 ] && d=$((0-d)); [ "$d" -le ${PAINT_SETTLE_DELTA_BYTES} ] && break; fi; ` +
312
+ `if [ "$sz" -ge ${PAINT_MIN_BYTES} ] && [ "$prev" -ge ${PAINT_MIN_BYTES} ]; then d=$((sz-prev)); [ "$d" -lt 0 ] && d=$((0-d)); if [ "$d" -le ${PAINT_SETTLE_DELTA_BYTES} ]; then ds_stage paint_ready; break; fi; fi; ` +
238
313
  `prev=$sz; ` +
239
314
  // NOTE: NOT_PAINTING goes to STDOUT (not stderr): Modal is execCommand-only, so the
240
315
  // caller infers the outcome by string-matching the output — stdout is always captured.
241
- `if [ "$i" = "${PAINT_PROBE_ATTEMPTS}" ]; then echo "OPENGENI_DESKTOP_NOT_PAINTING scrot below ${PAINT_MIN_BYTES}B or unsettled after warmup (last=$sz)"; exit 14; fi; ` +
316
+ `if [ "$i" = "${PAINT_PROBE_ATTEMPTS}" ]; then ds_stage paint_failed; echo "OPENGENI_DESKTOP_NOT_PAINTING scrot below ${PAINT_MIN_BYTES}B or unsettled after warmup (last=$sz)"; exit 14; fi; ` +
242
317
  `sleep ${PAINT_PROBE_INTERVAL_S}; ` +
243
318
  `done`;
244
- return `mkdir -p /tmp/opengeni-desktop; { ${bringUp} ; } && { ${paintProbe} ; }`;
319
+ const trace =
320
+ `ds_started=$(date +%s%3N); ds_class=unknown; ` +
321
+ `ds_stage() { ds_now=$(date +%s%3N); echo "OPENGENI_DISPLAY_STAGE stage=$1 elapsed_ms=$((ds_now-ds_started)) classification=$ds_class"; }; ` +
322
+ `ds_stage script_entry`;
323
+ return `${trace}; mkdir -p /tmp/opengeni-desktop; { ${bringUp} ; } && { ${paintProbe} ; }`;
245
324
  }
246
325
 
247
326
  function execResultOutput(result: ExecResultLike | string): string {
@@ -255,16 +334,617 @@ function execResultOutput(result: ExecResultLike | string): string {
255
334
 
256
335
  function execResultExitCode(result: ExecResultLike | string): number | null {
257
336
  if (typeof result === "string") {
258
- return null; // execCommand returns a bare string — no exit code available.
337
+ const banner = parseExecResponseBanner(result);
338
+ if (banner.kind === "exited") return banner.exitCode;
339
+ // A malformed/truncated SDK response must not fall through to command-body
340
+ // marker inference, because its real terminal status is unknowable.
341
+ return banner.kind === "invalid" ? -1 : null;
259
342
  }
260
343
  return typeof result.exitCode === "number" ? result.exitCode : null;
261
344
  }
262
345
 
346
+ function execResultSessionId(result: ExecResultLike | string): number | null {
347
+ if (typeof result === "string") {
348
+ const banner = parseExecResponseBanner(result);
349
+ return banner.kind === "running" ? banner.sessionId : null;
350
+ }
351
+ const value = result.sessionId ?? result.session_id;
352
+ return typeof value === "number" ? value : null;
353
+ }
354
+
355
+ function shellQuote(value: string): string {
356
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
357
+ }
358
+
359
+ /** The provider yield is not a deadline. Bound the actual lock-owning process in
360
+ * the box, leaving time for TERM/KILL cleanup and the final exit result to reach
361
+ * the caller before the outer JavaScript deadline. */
362
+ function boundedDisplayStackCommand(
363
+ command: string,
364
+ timeoutMs: number,
365
+ markerPath: string,
366
+ markerToken: string,
367
+ ): string {
368
+ const graceMs = Math.min(
369
+ DISPLAY_STACK_TERMINATION_GRACE_MS,
370
+ Math.max(20, Math.floor(timeoutMs / 4)),
371
+ );
372
+ const commandTimeoutMs = Math.max(1, timeoutMs - graceMs);
373
+ const killAfterMs = Math.max(1, Math.floor(graceMs / 2));
374
+ const commandTimeoutSeconds = (commandTimeoutMs / 1_000).toFixed(3);
375
+ const killAfterSeconds = (killAfterMs / 1_000).toFixed(3);
376
+ const managedCommand =
377
+ `marker=${shellQuote(markerPath)}; token=${shellQuote(markerToken)}; ` +
378
+ `cancel="$marker.cancelled"; ` +
379
+ `if [ -e "$cancel" ]; then echo "OPENGENI_DISPLAY_CANCELLED"; exit 124; fi; ` +
380
+ `pid=$$; pgid=$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' '); ` +
381
+ `start=$(awk '{print $22}' "/proc/$pid/stat" 2>/dev/null || true); ` +
382
+ `printf '%s %s %s %s\\n' "$pid" "$pgid" "$start" "$token" >"$marker"; ` +
383
+ `if [ -e "$cancel" ]; then rm -f "$marker"; echo "OPENGENI_DISPLAY_CANCELLED"; exit 124; fi; ` +
384
+ // Do not install an EXIT trap: GNU timeout may terminate this supervisor
385
+ // while descendants survive. The marker must remain for the independent
386
+ // cleanup command to find and kill that orphaned process group.
387
+ `bash -c ${shellQuote(command)}; rc=$?; rm -f "$marker"; exit "$rc"`;
388
+ return (
389
+ // Do not use timeout --foreground here. The display command contains flock
390
+ // supervisors and launcher children; default process-group mode is required
391
+ // so a deadline cannot leave a waiter alive to acquire the lock and launch
392
+ // after the caller has already failed.
393
+ `timeout --signal=TERM --kill-after=${killAfterSeconds}s ${commandTimeoutSeconds}s ` +
394
+ `bash -c ${shellQuote(managedCommand)}; rc=$?; ` +
395
+ // Exit 137 is also a normal SIGKILL/OOM/provider-termination result. Only
396
+ // attach timeout attribution to the exit code that this wrapper positively
397
+ // knows GNU timeout emitted for its own deadline. A kill-after escalation
398
+ // may still be reported as 137, but it is not safe to infer that here.
399
+ `if [ "$rc" -eq 124 ]; then ` +
400
+ `echo "OPENGENI_DISPLAY_TIMEOUT elapsed_ms=${commandTimeoutMs}"; fi; exit "$rc"`
401
+ );
402
+ }
403
+
404
+ class DisplayStackWallTimeoutError extends Error {}
405
+ class DisplayStackOperationFencedError extends Error {}
406
+
407
+ type PendingDisplayStackCleanup = {
408
+ markerPath: string;
409
+ markerToken: string;
410
+ };
411
+
412
+ type DisplayStackOperationWaiter = {
413
+ epoch: number;
414
+ settled: boolean;
415
+ resolve: () => void;
416
+ reject: (error: unknown) => void;
417
+ timer?: ReturnType<typeof setTimeout>;
418
+ };
419
+
420
+ type DisplayStackOperationState = {
421
+ /** Incremented before a timed-out operation is reported to its caller. */
422
+ epoch: number;
423
+ /** One provider operation at a time for each physical sandbox. */
424
+ owner: DisplayStackOperationWaiter | null;
425
+ /** Calls that started in the current epoch but are waiting for admission. */
426
+ waiters: DisplayStackOperationWaiter[];
427
+ /** Raw provider calls from fenced operations; only the in-box proof gates retries. */
428
+ inFlight: Set<Promise<unknown>>;
429
+ /** A bounded advisory drain for provider calls that may never settle. */
430
+ drain: Promise<void> | null;
431
+ /** Timed-out in-box processes that still need an independent kill/verify. */
432
+ pendingCleanup: Map<string, PendingDisplayStackCleanup>;
433
+ /** True only after every timed-out process has a positive quiescence proof. */
434
+ physicallyQuiescent: boolean;
435
+ };
436
+
437
+ const stableDisplayStackOperationStates = new Map<string, DisplayStackOperationState>();
438
+ const stableSessionStateOperationStates = new WeakMap<object, DisplayStackOperationState>();
439
+ const displayStackOperationStates = new WeakMap<object, DisplayStackOperationState>();
440
+
441
+ function asRecord(value: unknown): Record<string, unknown> | null {
442
+ return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : null;
443
+ }
444
+
445
+ function nonEmptyString(value: unknown): string | null {
446
+ return typeof value === "string" && value.length > 0 ? value : null;
447
+ }
448
+
449
+ function readSessionState(session: unknown): unknown {
450
+ try {
451
+ return asRecord(session)?.state;
452
+ } catch {
453
+ return undefined;
454
+ }
455
+ }
456
+
457
+ function stableSandboxIdentity(
458
+ session: unknown,
459
+ options: EnsureDisplayStackOptions,
460
+ ): string | null {
461
+ const explicit = nonEmptyString(options.telemetryContext?.sandboxId);
462
+ if (explicit) return `sandbox:${explicit}`;
463
+
464
+ const sessionRecord = asRecord(session);
465
+ const state = asRecord(readSessionState(session));
466
+ const providerState = state ? asRecord(state.providerState) : null;
467
+ const candidates = [sessionRecord, state, providerState];
468
+ const fields = [
469
+ "sandboxId",
470
+ "instanceId",
471
+ "agentId",
472
+ "hostId",
473
+ "containerId",
474
+ "id",
475
+ "workspaceRootPath",
476
+ ];
477
+ for (const candidate of candidates) {
478
+ if (!candidate) continue;
479
+ for (const field of fields) {
480
+ const value = nonEmptyString(candidate[field]);
481
+ if (value) return `sandbox:${value}`;
482
+ }
483
+ }
484
+ return null;
485
+ }
486
+
487
+ function newOperationState(): DisplayStackOperationState {
488
+ return {
489
+ epoch: 0,
490
+ owner: null,
491
+ waiters: [],
492
+ inFlight: new Set(),
493
+ drain: null,
494
+ pendingCleanup: new Map(),
495
+ physicallyQuiescent: true,
496
+ };
497
+ }
498
+
499
+ function operationState(
500
+ session: unknown,
501
+ options: EnsureDisplayStackOptions,
502
+ ): DisplayStackOperationState | null {
503
+ if ((typeof session !== "object" || session === null) && typeof session !== "function") {
504
+ return null;
505
+ }
506
+ const stableKey = stableSandboxIdentity(session, options);
507
+ if (stableKey) {
508
+ let state = stableDisplayStackOperationStates.get(stableKey);
509
+ if (!state) {
510
+ state = newOperationState();
511
+ stableDisplayStackOperationStates.set(stableKey, state);
512
+ }
513
+ return state;
514
+ }
515
+
516
+ // Modal resumed wrappers share their provider `state` object even when the
517
+ // wrapper instance is reconstructed. This is a useful stable fallback when
518
+ // an older/provider-specific state shape has no named identity field.
519
+ const providerState = readSessionState(session);
520
+ if (providerState && (typeof providerState === "object" || typeof providerState === "function")) {
521
+ const stateKey = providerState as object;
522
+ let state = stableSessionStateOperationStates.get(stateKey);
523
+ if (!state) {
524
+ state = newOperationState();
525
+ stableSessionStateOperationStates.set(stateKey, state);
526
+ }
527
+ return state;
528
+ }
529
+
530
+ const key = session as object;
531
+ let state = displayStackOperationStates.get(key);
532
+ if (!state) {
533
+ state = newOperationState();
534
+ displayStackOperationStates.set(key, state);
535
+ }
536
+ return state;
537
+ }
538
+
539
+ function settleOperationWaiter(waiter: DisplayStackOperationWaiter, error?: unknown): void {
540
+ if (waiter.settled) return;
541
+ waiter.settled = true;
542
+ if (waiter.timer) clearTimeout(waiter.timer);
543
+ if (error === undefined) waiter.resolve();
544
+ else waiter.reject(error);
545
+ }
546
+
547
+ function pumpOperationWaiters(state: DisplayStackOperationState): void {
548
+ if (state.owner) return;
549
+ while (state.waiters.length > 0) {
550
+ const waiter = state.waiters.shift()!;
551
+ if (waiter.settled) continue;
552
+ if (waiter.epoch !== state.epoch) {
553
+ settleOperationWaiter(waiter, new DisplayStackOperationFencedError());
554
+ continue;
555
+ }
556
+ state.owner = waiter;
557
+ settleOperationWaiter(waiter);
558
+ return;
559
+ }
560
+ }
561
+
562
+ function acquireOperation(
563
+ state: DisplayStackOperationState,
564
+ deadline: number,
565
+ ): Promise<() => void> {
566
+ return new Promise<() => void>((resolve, reject) => {
567
+ const waiter: DisplayStackOperationWaiter = {
568
+ epoch: state.epoch,
569
+ settled: false,
570
+ resolve: () => {
571
+ resolve(() => {
572
+ if (state.owner !== waiter) return;
573
+ state.owner = null;
574
+ pumpOperationWaiters(state);
575
+ });
576
+ },
577
+ reject,
578
+ };
579
+ state.waiters.push(waiter);
580
+ const remainingMs = deadline - monotonicNow();
581
+ if (remainingMs <= 0) {
582
+ waiter.settled = true;
583
+ state.waiters = state.waiters.filter((candidate) => candidate !== waiter);
584
+ reject(new DisplayStackWallTimeoutError());
585
+ return;
586
+ }
587
+ waiter.timer = setTimeout(() => {
588
+ if (waiter.settled) return;
589
+ waiter.settled = true;
590
+ state.waiters = state.waiters.filter((candidate) => candidate !== waiter);
591
+ reject(new DisplayStackWallTimeoutError());
592
+ }, remainingMs);
593
+ waiter.timer.unref?.();
594
+ pumpOperationWaiters(state);
595
+ });
596
+ }
597
+
598
+ function invalidateQueuedOperations(state: DisplayStackOperationState, epoch: number): void {
599
+ const retained: DisplayStackOperationWaiter[] = [];
600
+ for (const waiter of state.waiters) {
601
+ if (waiter.epoch === epoch)
602
+ settleOperationWaiter(waiter, new DisplayStackOperationFencedError());
603
+ else retained.push(waiter);
604
+ }
605
+ state.waiters = retained;
606
+ }
607
+
608
+ function monotonicNow(): number {
609
+ return performance.now();
610
+ }
611
+
612
+ async function waitForFencedOperation(
613
+ state: DisplayStackOperationState,
614
+ deadline: number,
615
+ ): Promise<boolean> {
616
+ const drain = state.drain;
617
+ if (!drain) return true;
618
+ try {
619
+ await beforeDeadline(drain, deadline);
620
+ } catch {
621
+ return false;
622
+ }
623
+ if (state.drain === drain) state.drain = null;
624
+ return true;
625
+ }
626
+
627
+ function trackProviderCall<T>(
628
+ state: DisplayStackOperationState | null,
629
+ promise: Promise<T>,
630
+ ): Promise<T> {
631
+ if (!state) return promise;
632
+ state.inFlight.add(promise);
633
+ void promise.then(
634
+ () => state.inFlight.delete(promise),
635
+ () => state.inFlight.delete(promise),
636
+ );
637
+ return promise;
638
+ }
639
+
640
+ function boundedProviderDrain(pending: Promise<unknown>[]): Promise<void> {
641
+ if (pending.length === 0) return Promise.resolve();
642
+ return new Promise<void>((resolve) => {
643
+ let remaining = pending.length;
644
+ let timer: ReturnType<typeof setTimeout> | undefined;
645
+ const finish = () => {
646
+ if (timer) clearTimeout(timer);
647
+ resolve();
648
+ };
649
+ timer = setTimeout(finish, DISPLAY_STACK_DRAIN_GRACE_MS);
650
+ timer.unref?.();
651
+ for (const promise of pending) {
652
+ void promise.then(
653
+ () => {
654
+ remaining -= 1;
655
+ if (remaining === 0) finish();
656
+ },
657
+ () => {
658
+ remaining -= 1;
659
+ if (remaining === 0) finish();
660
+ },
661
+ );
662
+ }
663
+ });
664
+ }
665
+
666
+ function fenceOperation(
667
+ state: DisplayStackOperationState | null,
668
+ epoch: number,
669
+ abortController: AbortController,
670
+ cleanup: PendingDisplayStackCleanup,
671
+ ): void {
672
+ abortController.abort(new DisplayStackWallTimeoutError());
673
+ if (!state) return;
674
+
675
+ if (state.epoch !== epoch) return;
676
+
677
+ state.pendingCleanup.set(cleanup.markerToken, cleanup);
678
+ state.physicallyQuiescent = false;
679
+ invalidateQueuedOperations(state, epoch);
680
+
681
+ // Advance the ownership epoch BEFORE the timeout error is surfaced. Any
682
+ // completion from this operation is now stale. A retry is gated by the
683
+ // independent in-box cleanup proof, not by an SDK promise that may hang.
684
+ state.epoch += 1;
685
+ const pending = [...state.inFlight];
686
+ const drain = boundedProviderDrain(pending);
687
+ state.drain = state.drain ? Promise.all([state.drain, drain]).then(() => undefined) : drain;
688
+ // A provider transport can retain an unresolved promise forever. Once the
689
+ // bounded advisory window ends, release those promises from the registry;
690
+ // the tombstone and in-box process-group proof remain the authoritative fence.
691
+ void drain.then(() => {
692
+ for (const promise of pending) state.inFlight.delete(promise);
693
+ });
694
+ }
695
+
696
+ function assertCurrentOperation(state: DisplayStackOperationState | null, epoch: number): void {
697
+ if (state && state.epoch !== epoch) throw new DisplayStackOperationFencedError();
698
+ }
699
+
700
+ async function beforeDeadline<T>(
701
+ promise: Promise<T>,
702
+ deadline: number,
703
+ now: () => number = monotonicNow,
704
+ ): Promise<T> {
705
+ const remainingMs = deadline - now();
706
+ if (remainingMs <= 0) throw new DisplayStackWallTimeoutError();
707
+ let timer: ReturnType<typeof setTimeout> | undefined;
708
+ try {
709
+ return await Promise.race([
710
+ promise,
711
+ new Promise<T>((_resolve, reject) => {
712
+ timer = setTimeout(() => reject(new DisplayStackWallTimeoutError()), remainingMs);
713
+ timer.unref?.();
714
+ }),
715
+ ]);
716
+ } finally {
717
+ if (timer) clearTimeout(timer);
718
+ }
719
+ }
720
+
721
+ function displayStackCleanupCommand(markerPath: string, markerToken: string): string {
722
+ const marker = shellQuote(markerPath);
723
+ const token = shellQuote(markerToken);
724
+ // The launcher writes pid, process-group id, /proc start time, and a random
725
+ // token before it starts the lock-owning command. The start-time check keeps
726
+ // a stale marker from killing an unrelated process after PID reuse. The
727
+ // process group is the authority: a provider session can disappear while
728
+ // descendants of the shell continue to hold the display lock.
729
+ const script =
730
+ `marker=${marker}; token=${token}; ` +
731
+ `cancel="$marker.cancelled"; ` +
732
+ `: >"$cancel" || { echo "OPENGENI_DISPLAY_CLEANUP status=invalid"; exit 75; }; ` +
733
+ `if [ ! -r "$marker" ]; then echo "OPENGENI_DISPLAY_CLEANUP status=stopped"; exit 0; fi; ` +
734
+ `read -r pid pgid start actual <"$marker" || { echo "OPENGENI_DISPLAY_CLEANUP status=invalid"; exit 75; }; ` +
735
+ `[ "$actual" = "$token" ] || { echo "OPENGENI_DISPLAY_CLEANUP status=invalid"; exit 75; }; ` +
736
+ `case "$pid" in ''|*[!0-9]*) echo "OPENGENI_DISPLAY_CLEANUP status=invalid"; exit 75;; esac; ` +
737
+ `case "$pgid" in ''|*[!0-9]*) echo "OPENGENI_DISPLAY_CLEANUP status=invalid"; exit 75;; esac; ` +
738
+ `case "$start" in ''|*[!0-9]*) echo "OPENGENI_DISPLAY_CLEANUP status=invalid"; exit 75;; esac; ` +
739
+ `current=$(awk '{print $22}' "/proc/$pid/stat" 2>/dev/null || true); ` +
740
+ `if [ -z "$current" ] || [ "$current" != "$start" ]; then rm -f "$marker"; echo "OPENGENI_DISPLAY_CLEANUP status=stopped"; exit 0; fi; ` +
741
+ `kill -TERM -- "-$pgid" 2>/dev/null || true; ` +
742
+ `for i in $(seq 1 10); do ` +
743
+ `if ! kill -0 -- "-$pgid" 2>/dev/null; then rm -f "$marker"; echo "OPENGENI_DISPLAY_CLEANUP status=stopped"; exit 0; fi; ` +
744
+ `sleep 0.05; done; ` +
745
+ `kill -KILL -- "-$pgid" 2>/dev/null || true; ` +
746
+ `for i in $(seq 1 20); do ` +
747
+ `if ! kill -0 -- "-$pgid" 2>/dev/null; then rm -f "$marker"; echo "OPENGENI_DISPLAY_CLEANUP status=stopped"; exit 0; fi; ` +
748
+ `sleep 0.05; done; ` +
749
+ `echo "OPENGENI_DISPLAY_CLEANUP status=alive"; exit 75`;
750
+ return `timeout --signal=TERM --kill-after=0.2s 1.5s bash -c ${shellQuote(script)}`;
751
+ }
752
+
753
+ async function runProviderCommandToDeadline(
754
+ session: ExecCapableSession,
755
+ command: string,
756
+ deadline: number,
757
+ ): Promise<ExecResultLike | string> {
758
+ const controller = new AbortController();
759
+ let result: ExecResultLike | string = await beforeDeadline(
760
+ Promise.resolve(
761
+ typeof session.exec === "function"
762
+ ? session.exec({
763
+ cmd: command,
764
+ yieldTimeMs: Math.min(500, DISPLAY_STACK_CLEANUP_CALL_MS),
765
+ maxOutputTokens: 4_000,
766
+ signal: controller.signal,
767
+ })
768
+ : session.execCommand!({
769
+ cmd: command,
770
+ yieldTimeMs: Math.min(500, DISPLAY_STACK_CLEANUP_CALL_MS),
771
+ maxOutputTokens: 4_000,
772
+ signal: controller.signal,
773
+ }),
774
+ ),
775
+ deadline,
776
+ );
777
+
778
+ let providerSessionId = execResultSessionId(result);
779
+ while (providerSessionId !== null) {
780
+ if (typeof session.writeStdin !== "function") {
781
+ throw new Error("display cleanup provider yielded without writeStdin");
782
+ }
783
+ const remainingMs = deadline - monotonicNow();
784
+ if (remainingMs <= 1) throw new DisplayStackWallTimeoutError();
785
+ result = await beforeDeadline(
786
+ session.writeStdin({
787
+ sessionId: providerSessionId,
788
+ chars: "",
789
+ yieldTimeMs: Math.max(1, Math.min(250, remainingMs - 1)),
790
+ maxOutputTokens: 4_000,
791
+ signal: controller.signal,
792
+ }),
793
+ deadline,
794
+ );
795
+ providerSessionId = execResultSessionId(result);
796
+ }
797
+ return result;
798
+ }
799
+
800
+ async function interruptProviderProcess(
801
+ session: ExecCapableSession,
802
+ providerSessionId: number | null,
803
+ deadline: number,
804
+ ): Promise<void> {
805
+ if (providerSessionId === null || typeof session.writeStdin !== "function") return;
806
+ try {
807
+ // This is an advisory provider-level interrupt. The marker/PGID cleanup
808
+ // below remains authoritative because Modal 0.13.3 ignores AbortSignal.
809
+ await beforeDeadline(
810
+ session.writeStdin({
811
+ sessionId: providerSessionId,
812
+ chars: "\u0003",
813
+ yieldTimeMs: 100,
814
+ maxOutputTokens: 256,
815
+ }),
816
+ deadline,
817
+ );
818
+ } catch {
819
+ // A hung provider write is not evidence either way; in-box cleanup decides.
820
+ }
821
+ }
822
+
823
+ async function cleanupDisplayStackProcess(
824
+ session: ExecCapableSession,
825
+ pending: PendingDisplayStackCleanup,
826
+ deadline: number,
827
+ ): Promise<boolean> {
828
+ try {
829
+ const result = await runProviderCommandToDeadline(
830
+ session,
831
+ displayStackCleanupCommand(pending.markerPath, pending.markerToken),
832
+ deadline,
833
+ );
834
+ const output = execResultOutput(result);
835
+ const status = /OPENGENI_DISPLAY_CLEANUP status=(stopped|absent|alive|invalid)/u.exec(
836
+ output,
837
+ )?.[1];
838
+ return status === "stopped";
839
+ } catch {
840
+ return false;
841
+ }
842
+ }
843
+
844
+ async function reconcilePendingCleanup(
845
+ session: ExecCapableSession,
846
+ state: DisplayStackOperationState,
847
+ deadline: number,
848
+ ): Promise<boolean> {
849
+ const entries = [...state.pendingCleanup.values()];
850
+ if (entries.length === 0) {
851
+ state.physicallyQuiescent = true;
852
+ return true;
853
+ }
854
+ const results = await Promise.all(
855
+ entries.map(async (pending) => ({
856
+ pending,
857
+ stopped: await cleanupDisplayStackProcess(session, pending, deadline),
858
+ })),
859
+ );
860
+ for (const { pending, stopped } of results) {
861
+ if (stopped) state.pendingCleanup.delete(pending.markerToken);
862
+ }
863
+ state.physicallyQuiescent = state.pendingCleanup.size === 0;
864
+ return state.physicallyQuiescent;
865
+ }
866
+
867
+ function requestId(): string {
868
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
869
+ }
870
+
871
+ function telemetryReporter(
872
+ options: EnsureDisplayStackOptions,
873
+ started: number,
874
+ now: () => number = monotonicNow,
875
+ ) {
876
+ const id = requestId();
877
+ const context = options.telemetryContext;
878
+ const sink =
879
+ options.onTelemetry ??
880
+ (context
881
+ ? (event: DisplayStackTelemetryEvent) =>
882
+ console.info(`[display-stack] ${JSON.stringify(event)}`)
883
+ : undefined);
884
+ const emittedSandboxStages = new Set<string>();
885
+
886
+ const emit = (
887
+ stage: string,
888
+ status: DisplayStackTelemetryStatus,
889
+ fields: {
890
+ source?: "host" | "sandbox";
891
+ elapsedMs?: number;
892
+ classification?: DisplayStackClassification;
893
+ providerSessionId?: number;
894
+ } = {},
895
+ ) => {
896
+ if (!sink) return;
897
+ const event: DisplayStackTelemetryEvent = {
898
+ type: "display_stack.stage",
899
+ requestId: id,
900
+ stage,
901
+ status,
902
+ elapsedMs: fields.elapsedMs ?? now() - started,
903
+ source: fields.source ?? "host",
904
+ classification: fields.classification ?? "unknown",
905
+ callerKind: context?.callerKind ?? "unknown",
906
+ ...(context?.sandboxId ? { sandboxId: context.sandboxId } : {}),
907
+ ...(context?.leaseEpoch !== undefined ? { leaseEpoch: context.leaseEpoch } : {}),
908
+ ...(fields.providerSessionId !== undefined
909
+ ? { providerSessionId: fields.providerSessionId }
910
+ : {}),
911
+ };
912
+ try {
913
+ void Promise.resolve(sink(event)).catch(() => undefined);
914
+ } catch {
915
+ // Telemetry must never affect display readiness.
916
+ }
917
+ };
918
+
919
+ const emitSandboxStages = (output: string) => {
920
+ for (const match of output.matchAll(
921
+ /OPENGENI_(?:DISPLAY|DESKTOP)_STAGE stage=([a-z0-9_]+) elapsed_ms=(\d+)(?: classification=(cold|already_ready|contention|unknown))?/gu,
922
+ )) {
923
+ const [line, stage, elapsed, classification] = match;
924
+ if (!stage || !elapsed || emittedSandboxStages.has(line)) continue;
925
+ emittedSandboxStages.add(line);
926
+ emit(stage, "completed", {
927
+ source: "sandbox",
928
+ elapsedMs: Number(elapsed),
929
+ classification: (classification as DisplayStackClassification | undefined) ?? "unknown",
930
+ });
931
+ }
932
+ };
933
+
934
+ return { emit, emitSandboxStages };
935
+ }
936
+
263
937
  // Parse the exit code the up-script signals via its trailing marker. When we ran
264
938
  // through `exec` we have the real exitCode; when we only had `execCommand` (a
265
939
  // bare string), we infer success from the OPENGENI_DESKTOP_UP marker and infer
266
940
  // the failing stage from the stage-failure message the script prints to stderr.
267
941
  function inferExitFromOutput(output: string): number {
942
+ if (/OPENGENI_DISPLAY_TIMEOUT/.test(output)) {
943
+ return 124;
944
+ }
945
+ if (/OPENGENI_DESKTOP_LOCK_TIMEOUT/.test(output)) {
946
+ return 15;
947
+ }
268
948
  // Check the PAINTABLE-FRAME failure FIRST: on that path the up-script already
269
949
  // printed OPENGENI_DESKTOP_UP (bring-up succeeded) and THEN the paint gate failed,
270
950
  // so both markers are present — the NOT_PAINTING one is the authoritative outcome.
@@ -314,22 +994,179 @@ export async function ensureDisplayStack(
314
994
  const geometry = options.geometry ?? DEFAULT_DESKTOP_GEOMETRY;
315
995
  const port = options.port ?? DESKTOP_STREAM_PORT;
316
996
  const timeoutMs = options.timeoutMs ?? DISPLAY_STACK_TIMEOUT_MS;
317
- const cmd = buildDisplayStackScript({ geometry, port });
997
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
998
+ throw new RangeError("ensureDisplayStack timeoutMs must be a positive finite number");
999
+ }
1000
+ const started = monotonicNow();
1001
+ const deadline = started + timeoutMs;
1002
+ const telemetry = telemetryReporter(options, started, monotonicNow);
1003
+ const operationToken = requestId();
1004
+ const markerPath = `/tmp/opengeni-desktop/display-stack-${operationToken}.pid`;
1005
+ const cmd = boundedDisplayStackCommand(
1006
+ buildDisplayStackScript({ geometry, port }),
1007
+ timeoutMs,
1008
+ markerPath,
1009
+ operationToken,
1010
+ );
1011
+ const outputParts: string[] = [];
1012
+ const state = operationState(session, options);
1013
+ let release: (() => void) | undefined;
1014
+ if (state) {
1015
+ try {
1016
+ release = await acquireOperation(state, deadline);
1017
+ } catch (error) {
1018
+ if (
1019
+ error instanceof DisplayStackWallTimeoutError ||
1020
+ error instanceof DisplayStackOperationFencedError
1021
+ ) {
1022
+ throw new DisplayStackError(
1023
+ 124,
1024
+ "OPENGENI_DISPLAY_TIMEOUT display operation admission was fenced",
1025
+ );
1026
+ }
1027
+ throw error;
1028
+ }
1029
+ }
318
1030
 
319
- const result =
320
- typeof s.exec === "function"
321
- ? await s.exec({ cmd, yieldTimeMs: timeoutMs, maxOutputTokens: 20_000 })
322
- : await s.execCommand!({ cmd, yieldTimeMs: timeoutMs, maxOutputTokens: 20_000 });
1031
+ try {
1032
+ if (state && !state.physicallyQuiescent) {
1033
+ const cleanupDeadline = monotonicNow() + DISPLAY_STACK_CLEANUP_CALL_MS;
1034
+ const quiesced = await reconcilePendingCleanup(s, state, cleanupDeadline);
1035
+ if (!quiesced) {
1036
+ // Fail closed. A new session wrapper may share this physical sandbox,
1037
+ // but without a positive marker/PGID proof it must not issue overlapping
1038
+ // display work merely because the old SDK promise remains unresolved.
1039
+ throw new DisplayStackError(
1040
+ 124,
1041
+ "OPENGENI_DISPLAY_TIMEOUT physical cleanup is still pending; retry is fenced",
1042
+ );
1043
+ }
1044
+ }
1045
+ const epoch = state?.epoch ?? 0;
1046
+ const abortController = new AbortController();
1047
+ let activeProviderSessionId: number | null = null;
1048
+ telemetry.emit("request_entry", "started");
1049
+ telemetry.emit("exec_issued", "started");
323
1050
 
324
- const output = execResultOutput(result);
325
- const exitCode = execResultExitCode(result) ?? inferExitFromOutput(output);
1051
+ try {
1052
+ const initialYieldMs = Math.max(1, Math.min(DISPLAY_STACK_PROVIDER_YIELD_MS, timeoutMs - 1));
1053
+ const initialExec: Promise<ExecResultLike | string> = trackProviderCall<
1054
+ ExecResultLike | string
1055
+ >(
1056
+ state,
1057
+ Promise.resolve(
1058
+ typeof s.exec === "function"
1059
+ ? s.exec({
1060
+ cmd,
1061
+ yieldTimeMs: initialYieldMs,
1062
+ maxOutputTokens: 20_000,
1063
+ signal: abortController.signal,
1064
+ })
1065
+ : s.execCommand!({
1066
+ cmd,
1067
+ yieldTimeMs: initialYieldMs,
1068
+ maxOutputTokens: 20_000,
1069
+ signal: abortController.signal,
1070
+ }),
1071
+ ),
1072
+ );
1073
+ let result: ExecResultLike | string = await beforeDeadline(
1074
+ initialExec,
1075
+ deadline,
1076
+ monotonicNow,
1077
+ );
1078
+ assertCurrentOperation(state, epoch);
1079
+ let chunk = execResultOutput(result);
1080
+ outputParts.push(chunk);
1081
+ telemetry.emitSandboxStages(chunk);
1082
+ telemetry.emit("provider_first_result", "completed");
326
1083
 
327
- if (exitCode !== 0) {
328
- throw new DisplayStackError(exitCode, output);
329
- }
1084
+ let providerSessionId = execResultSessionId(result);
1085
+ activeProviderSessionId = providerSessionId;
1086
+ while (providerSessionId !== null) {
1087
+ telemetry.emit("provider_yield", "waiting", { providerSessionId });
1088
+ if (typeof s.writeStdin !== "function") {
1089
+ throw new DisplayStackError(
1090
+ -1,
1091
+ `${outputParts.join("\n")}\nprovider yielded process ${providerSessionId} but exposes no writeStdin poll surface`,
1092
+ );
1093
+ }
1094
+ assertCurrentOperation(state, epoch);
1095
+ const remainingMs = deadline - monotonicNow();
1096
+ if (remainingMs <= 1) throw new DisplayStackWallTimeoutError();
1097
+ const pollYieldMs = Math.max(1, Math.min(DISPLAY_STACK_PROVIDER_POLL_MS, remainingMs - 1));
1098
+ result = await beforeDeadline(
1099
+ trackProviderCall(
1100
+ state,
1101
+ s.writeStdin({
1102
+ sessionId: providerSessionId,
1103
+ chars: "",
1104
+ yieldTimeMs: pollYieldMs,
1105
+ maxOutputTokens: 20_000,
1106
+ signal: abortController.signal,
1107
+ }),
1108
+ ),
1109
+ deadline,
1110
+ monotonicNow,
1111
+ );
1112
+ assertCurrentOperation(state, epoch);
1113
+ chunk = execResultOutput(result);
1114
+ outputParts.push(chunk);
1115
+ telemetry.emitSandboxStages(chunk);
1116
+ providerSessionId = execResultSessionId(result);
1117
+ activeProviderSessionId = providerSessionId;
1118
+ }
1119
+
1120
+ const output = outputParts.join("\n");
1121
+ const exitCode = execResultExitCode(result) ?? inferExitFromOutput(output);
1122
+ telemetry.emit("process_complete", exitCode === 0 ? "completed" : "failed");
1123
+ if (exitCode !== 0) {
1124
+ throw new DisplayStackError(exitCode, output);
1125
+ }
330
1126
 
331
- const marker = (output.match(/OPENGENI_DESKTOP_UP[^\n]*/) ?? [""])[0];
332
- return { port, geometry, marker };
1127
+ const marker = (output.match(/OPENGENI_DESKTOP_UP[^\n]*/) ?? [""])[0];
1128
+ telemetry.emit("readiness_complete", "completed");
1129
+ return { port, geometry, marker };
1130
+ } catch (error) {
1131
+ if (
1132
+ error instanceof DisplayStackWallTimeoutError ||
1133
+ error instanceof DisplayStackOperationFencedError
1134
+ ) {
1135
+ const pendingCleanup: PendingDisplayStackCleanup = {
1136
+ markerPath,
1137
+ markerToken: operationToken,
1138
+ };
1139
+ fenceOperation(state, epoch, abortController, pendingCleanup);
1140
+ const cleanupDeadline = monotonicNow() + DISPLAY_STACK_CLEANUP_CALL_MS;
1141
+ telemetry.emit("process_cleanup", "started");
1142
+ await interruptProviderProcess(
1143
+ s,
1144
+ activeProviderSessionId,
1145
+ Math.min(cleanupDeadline, monotonicNow() + 50),
1146
+ );
1147
+ let quiesced = false;
1148
+ if (state) {
1149
+ quiesced = await reconcilePendingCleanup(s, state, cleanupDeadline);
1150
+ if (!quiesced) {
1151
+ await waitForFencedOperation(
1152
+ state,
1153
+ Math.min(cleanupDeadline, monotonicNow() + DISPLAY_STACK_DRAIN_GRACE_MS),
1154
+ );
1155
+ }
1156
+ } else {
1157
+ quiesced = await cleanupDisplayStackProcess(s, pendingCleanup, cleanupDeadline);
1158
+ }
1159
+ telemetry.emit("process_cleanup", quiesced ? "completed" : "failed");
1160
+ const output = `${outputParts.join("\n")}\nOPENGENI_DISPLAY_TIMEOUT wall_ms=${timeoutMs}`;
1161
+ telemetry.emit("wall_deadline", "failed");
1162
+ throw new DisplayStackError(124, output);
1163
+ }
1164
+ telemetry.emit("request_failed", "failed");
1165
+ throw error;
1166
+ }
1167
+ } finally {
1168
+ release?.();
1169
+ }
333
1170
  }
334
1171
 
335
1172
  /** Tear the stack down (down-script). Best-effort; never throws on a missing