tywrap 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +65 -31
  2. package/dist/cli.js +49 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.d.ts +11 -7
  5. package/dist/config/index.d.ts.map +1 -1
  6. package/dist/config/index.js +43 -7
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/core/analyzer.d.ts +1 -0
  9. package/dist/core/analyzer.d.ts.map +1 -1
  10. package/dist/core/analyzer.js +47 -12
  11. package/dist/core/analyzer.js.map +1 -1
  12. package/dist/core/annotation-parser.d.ts +8 -0
  13. package/dist/core/annotation-parser.d.ts.map +1 -0
  14. package/dist/core/annotation-parser.js +409 -0
  15. package/dist/core/annotation-parser.js.map +1 -0
  16. package/dist/core/discovery.d.ts +1 -0
  17. package/dist/core/discovery.d.ts.map +1 -1
  18. package/dist/core/discovery.js +9 -9
  19. package/dist/core/discovery.js.map +1 -1
  20. package/dist/core/generator.d.ts +9 -1
  21. package/dist/core/generator.d.ts.map +1 -1
  22. package/dist/core/generator.js +738 -104
  23. package/dist/core/generator.js.map +1 -1
  24. package/dist/core/mapper.d.ts +9 -7
  25. package/dist/core/mapper.d.ts.map +1 -1
  26. package/dist/core/mapper.js +129 -44
  27. package/dist/core/mapper.js.map +1 -1
  28. package/dist/index.d.ts +7 -7
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +4 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/runtime/bridge-core.d.ts +1 -0
  33. package/dist/runtime/bridge-core.d.ts.map +1 -1
  34. package/dist/runtime/bridge-core.js +52 -17
  35. package/dist/runtime/bridge-core.js.map +1 -1
  36. package/dist/runtime/bridge-protocol.d.ts +17 -0
  37. package/dist/runtime/bridge-protocol.d.ts.map +1 -1
  38. package/dist/runtime/bridge-protocol.js +97 -0
  39. package/dist/runtime/bridge-protocol.js.map +1 -1
  40. package/dist/runtime/errors.d.ts +10 -0
  41. package/dist/runtime/errors.d.ts.map +1 -1
  42. package/dist/runtime/errors.js +9 -0
  43. package/dist/runtime/errors.js.map +1 -1
  44. package/dist/runtime/http-io.d.ts.map +1 -1
  45. package/dist/runtime/http-io.js.map +1 -1
  46. package/dist/runtime/http.d.ts +1 -0
  47. package/dist/runtime/http.d.ts.map +1 -1
  48. package/dist/runtime/http.js +6 -0
  49. package/dist/runtime/http.js.map +1 -1
  50. package/dist/runtime/node.d.ts.map +1 -1
  51. package/dist/runtime/node.js +170 -32
  52. package/dist/runtime/node.js.map +1 -1
  53. package/dist/runtime/optimized-node.d.ts +1 -1
  54. package/dist/runtime/optimized-node.d.ts.map +1 -1
  55. package/dist/runtime/optimized-node.js +1 -1
  56. package/dist/runtime/optimized-node.js.map +1 -1
  57. package/dist/runtime/pooled-transport.d.ts +4 -0
  58. package/dist/runtime/pooled-transport.d.ts.map +1 -1
  59. package/dist/runtime/pooled-transport.js +2 -0
  60. package/dist/runtime/pooled-transport.js.map +1 -1
  61. package/dist/runtime/process-io.d.ts +5 -0
  62. package/dist/runtime/process-io.d.ts.map +1 -1
  63. package/dist/runtime/process-io.js +64 -30
  64. package/dist/runtime/process-io.js.map +1 -1
  65. package/dist/runtime/pyodide-io.d.ts +4 -5
  66. package/dist/runtime/pyodide-io.d.ts.map +1 -1
  67. package/dist/runtime/pyodide-io.js +99 -37
  68. package/dist/runtime/pyodide-io.js.map +1 -1
  69. package/dist/runtime/safe-codec.d.ts +25 -3
  70. package/dist/runtime/safe-codec.d.ts.map +1 -1
  71. package/dist/runtime/safe-codec.js +231 -57
  72. package/dist/runtime/safe-codec.js.map +1 -1
  73. package/dist/runtime/transport.d.ts +6 -1
  74. package/dist/runtime/transport.d.ts.map +1 -1
  75. package/dist/runtime/transport.js.map +1 -1
  76. package/dist/runtime/validators.d.ts +0 -11
  77. package/dist/runtime/validators.d.ts.map +1 -1
  78. package/dist/runtime/validators.js +13 -3
  79. package/dist/runtime/validators.js.map +1 -1
  80. package/dist/runtime/worker-pool.d.ts +12 -0
  81. package/dist/runtime/worker-pool.d.ts.map +1 -1
  82. package/dist/runtime/worker-pool.js +53 -5
  83. package/dist/runtime/worker-pool.js.map +1 -1
  84. package/dist/types/index.d.ts +51 -1
  85. package/dist/types/index.d.ts.map +1 -1
  86. package/dist/tywrap.d.ts.map +1 -1
  87. package/dist/tywrap.js +227 -328
  88. package/dist/tywrap.js.map +1 -1
  89. package/dist/utils/cache.d.ts.map +1 -1
  90. package/dist/utils/cache.js +53 -5
  91. package/dist/utils/cache.js.map +1 -1
  92. package/dist/utils/codec.d.ts.map +1 -1
  93. package/dist/utils/codec.js +10 -16
  94. package/dist/utils/codec.js.map +1 -1
  95. package/dist/utils/ir-cache.d.ts +28 -0
  96. package/dist/utils/ir-cache.d.ts.map +1 -0
  97. package/dist/utils/ir-cache.js +29 -0
  98. package/dist/utils/ir-cache.js.map +1 -0
  99. package/dist/utils/parallel-processor.d.ts +1 -1
  100. package/dist/utils/parallel-processor.d.ts.map +1 -1
  101. package/dist/utils/parallel-processor.js +111 -40
  102. package/dist/utils/parallel-processor.js.map +1 -1
  103. package/dist/utils/runtime.d.ts +5 -1
  104. package/dist/utils/runtime.d.ts.map +1 -1
  105. package/dist/utils/runtime.js +112 -18
  106. package/dist/utils/runtime.js.map +1 -1
  107. package/package.json +40 -18
  108. package/runtime/python_bridge.py +78 -12
  109. package/src/cli.ts +61 -2
  110. package/src/config/index.ts +63 -17
  111. package/src/core/analyzer.ts +52 -15
  112. package/src/core/annotation-parser.ts +500 -0
  113. package/src/core/discovery.ts +26 -12
  114. package/src/core/generator.ts +956 -116
  115. package/src/core/mapper.ts +158 -51
  116. package/src/index.ts +34 -5
  117. package/src/runtime/bridge-core.ts +58 -18
  118. package/src/runtime/bridge-protocol.ts +164 -0
  119. package/src/runtime/errors.ts +14 -0
  120. package/src/runtime/http-io.ts +2 -1
  121. package/src/runtime/http.ts +7 -0
  122. package/src/runtime/node.ts +264 -44
  123. package/src/runtime/optimized-node.ts +4 -1
  124. package/src/runtime/pooled-transport.ts +12 -1
  125. package/src/runtime/process-io.ts +83 -55
  126. package/src/runtime/pyodide-io.ts +110 -44
  127. package/src/runtime/safe-codec.ts +290 -82
  128. package/src/runtime/transport.ts +6 -1
  129. package/src/runtime/validators.ts +14 -3
  130. package/src/runtime/worker-pool.ts +67 -6
  131. package/src/types/global.d.ts +11 -1
  132. package/src/types/index.ts +63 -0
  133. package/src/tywrap.ts +279 -358
  134. package/src/utils/cache.ts +59 -5
  135. package/src/utils/codec.ts +11 -15
  136. package/src/utils/ir-cache.ts +51 -0
  137. package/src/utils/parallel-processor.ts +119 -43
  138. package/src/utils/runtime.ts +129 -18
@@ -135,7 +135,8 @@ export class HttpIO implements Transport {
135
135
 
136
136
  // Determine effective timeout
137
137
  // 0 = no timeout (per interface contract), negative = use default
138
- const effectiveTimeout = timeoutMs === 0 ? 0 : timeoutMs > 0 ? timeoutMs : this.defaultTimeoutMs;
138
+ const effectiveTimeout =
139
+ timeoutMs === 0 ? 0 : timeoutMs > 0 ? timeoutMs : this.defaultTimeoutMs;
139
140
 
140
141
  // Create abort controller for timeout
141
142
  const controller = new AbortController();
@@ -10,6 +10,7 @@
10
10
  import { BridgeProtocol, type BridgeProtocolOptions } from './bridge-protocol.js';
11
11
  import { HttpIO } from './http-io.js';
12
12
  import type { CodecOptions } from './safe-codec.js';
13
+ import { autoRegisterArrowDecoder } from '../utils/codec.js';
13
14
 
14
15
  // =============================================================================
15
16
  // OPTIONS
@@ -83,4 +84,10 @@ export class HttpBridge extends BridgeProtocol {
83
84
 
84
85
  super(protocolOptions);
85
86
  }
87
+
88
+ protected async doInit(): Promise<void> {
89
+ // Best-effort: keep apache-arrow optional and avoid breaking non-Node runtimes.
90
+ await autoRegisterArrowDecoder();
91
+ await super.doInit();
92
+ }
86
93
  }
@@ -18,10 +18,11 @@ import { getVenvBinDir, getVenvPythonExe } from '../utils/runtime.js';
18
18
  import { globalCache } from '../utils/cache.js';
19
19
 
20
20
  import { BridgeProtocol, type BridgeProtocolOptions } from './bridge-protocol.js';
21
- import { BridgeProtocolError } from './errors.js';
21
+ import { BridgeExecutionError, BridgeProtocolError } from './errors.js';
22
22
  import { ProcessIO } from './process-io.js';
23
23
  import { PooledTransport } from './pooled-transport.js';
24
24
  import type { CodecOptions } from './safe-codec.js';
25
+ import { PROTOCOL_ID } from './transport.js';
25
26
  import type { PooledWorker } from './worker-pool.js';
26
27
 
27
28
  // =============================================================================
@@ -73,8 +74,7 @@ export interface NodeBridgeOptions {
73
74
 
74
75
  /** Commands to run on each process at startup for warming up. */
75
76
  warmupCommands?: Array<
76
- | { module: string; functionName: string; args?: unknown[] }
77
- | { method: string; params: unknown } // Legacy format for backwards compatibility
77
+ { module: string; functionName: string; args?: unknown[] } | { method: string; params: unknown } // Legacy shape preserved so runtime can surface a migration error
78
78
  >;
79
79
 
80
80
  // ===========================================================================
@@ -120,12 +120,17 @@ interface ResolvedOptions {
120
120
  enableCache: boolean;
121
121
  env: Record<string, string | undefined>;
122
122
  codec?: CodecOptions;
123
- warmupCommands: Array<
124
- | { module: string; functionName: string; args?: unknown[] }
125
- | { method: string; params: unknown }
126
- >;
123
+ warmupCommands: WarmupCommand[];
124
+ }
125
+
126
+ interface WarmupCommand {
127
+ module: string;
128
+ functionName: string;
129
+ args?: unknown[];
127
130
  }
128
131
 
132
+ const WORKER_READY_TIMEOUT_MS = 5000;
133
+
129
134
  // =============================================================================
130
135
  // UTILITIES
131
136
  // =============================================================================
@@ -158,6 +163,10 @@ function resolveVirtualEnv(
158
163
  * Get the environment variable key for PATH (case-insensitive on Windows).
159
164
  */
160
165
  function getPathKey(env: Record<string, string | undefined>): string {
166
+ if (Object.prototype.hasOwnProperty.call(env, 'PATH')) {
167
+ return 'PATH';
168
+ }
169
+
161
170
  for (const key of Object.keys(env)) {
162
171
  if (key.toLowerCase() === 'path') {
163
172
  return key;
@@ -166,6 +175,93 @@ function getPathKey(env: Record<string, string | undefined>): string {
166
175
  return 'PATH';
167
176
  }
168
177
 
178
+ function setPathValue(env: Record<string, string>, value: string): void {
179
+ setEnvValue(env, 'PATH', value);
180
+
181
+ if (process.platform !== 'win32') {
182
+ return;
183
+ }
184
+
185
+ for (const key of Object.keys(env)) {
186
+ if (key !== 'PATH' && key.toLowerCase() === 'path') {
187
+ setEnvValue(env, key, value);
188
+ }
189
+ }
190
+ }
191
+
192
+ const DANGEROUS_ENV_OVERRIDE_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
193
+
194
+ function createNullPrototypeEnv(): Record<string, string> {
195
+ return Object.create(null) as Record<string, string>;
196
+ }
197
+
198
+ function setEnvValue(env: Record<string, string>, key: string, value: string): void {
199
+ Object.defineProperty(env, key, {
200
+ value,
201
+ writable: true,
202
+ enumerable: true,
203
+ configurable: true,
204
+ });
205
+ }
206
+
207
+ function getEnvValue(env: Record<string, string>, key: string): string | undefined {
208
+ const value = Reflect.get(env, key);
209
+ return typeof value === 'string' ? value : undefined;
210
+ }
211
+
212
+ function assertSafeEnvOverrideKey(key: string): void {
213
+ if (DANGEROUS_ENV_OVERRIDE_KEYS.has(key)) {
214
+ throw new BridgeProtocolError(`Invalid environment override key "${key}" in options.env`);
215
+ }
216
+ }
217
+
218
+ function normalizeWarmupCommands(commands: NodeBridgeOptions['warmupCommands']): WarmupCommand[] {
219
+ if (commands === undefined) {
220
+ return [];
221
+ }
222
+ if (!Array.isArray(commands)) {
223
+ throw new BridgeProtocolError('warmupCommands must be an array when provided');
224
+ }
225
+
226
+ const warmups = commands;
227
+ return warmups.map((command, index) => {
228
+ if (!command || typeof command !== 'object' || Array.isArray(command)) {
229
+ throw new BridgeProtocolError(
230
+ `Invalid warmup command at index ${index + 1}: expected { module, functionName, args? }`
231
+ );
232
+ }
233
+
234
+ const candidate = command as Record<string, unknown>;
235
+ if ('method' in candidate || 'params' in candidate) {
236
+ throw new BridgeProtocolError(
237
+ `Invalid warmup command at index ${index + 1}: legacy { method, params } format is no longer supported. Use { module, functionName, args? }.`
238
+ );
239
+ }
240
+
241
+ if (typeof candidate.module !== 'string' || candidate.module.trim().length === 0) {
242
+ throw new BridgeProtocolError(
243
+ `Invalid warmup command at index ${index + 1}: "module" must be a non-empty string`
244
+ );
245
+ }
246
+ if (typeof candidate.functionName !== 'string' || candidate.functionName.trim().length === 0) {
247
+ throw new BridgeProtocolError(
248
+ `Invalid warmup command at index ${index + 1}: "functionName" must be a non-empty string`
249
+ );
250
+ }
251
+ if (candidate.args !== undefined && !Array.isArray(candidate.args)) {
252
+ throw new BridgeProtocolError(
253
+ `Invalid warmup command at index ${index + 1}: "args" must be an array when provided`
254
+ );
255
+ }
256
+
257
+ return {
258
+ module: candidate.module,
259
+ functionName: candidate.functionName,
260
+ args: candidate.args as unknown[] | undefined,
261
+ };
262
+ });
263
+ }
264
+
169
265
  // =============================================================================
170
266
  // NODE BRIDGE
171
267
  // =============================================================================
@@ -226,6 +322,8 @@ export class NodeBridge extends BridgeProtocol {
226
322
  const maxProcesses = options.maxProcesses ?? 1;
227
323
  const minProcesses = Math.min(options.minProcesses ?? 1, maxProcesses);
228
324
 
325
+ const warmupCommands = normalizeWarmupCommands(options.warmupCommands);
326
+
229
327
  const resolvedOptions: ResolvedOptions = {
230
328
  minProcesses,
231
329
  maxProcesses,
@@ -240,20 +338,22 @@ export class NodeBridge extends BridgeProtocol {
240
338
  enableCache: options.enableCache ?? false,
241
339
  env: options.env ?? {},
242
340
  codec: options.codec,
243
- warmupCommands: options.warmupCommands ?? [],
341
+ warmupCommands,
244
342
  };
245
343
 
246
344
  // Build environment for ProcessIO
247
345
  const processEnv = buildProcessEnv(resolvedOptions);
248
346
 
249
347
  // Create warmup callback for per-worker initialization
250
- const onWorkerReady = resolvedOptions.warmupCommands.length > 0
251
- ? createWarmupCallback(resolvedOptions.warmupCommands, resolvedOptions.timeoutMs)
252
- : undefined;
348
+ const userWarmup =
349
+ resolvedOptions.warmupCommands.length > 0
350
+ ? createWarmupCallback(resolvedOptions.warmupCommands, resolvedOptions.timeoutMs)
351
+ : undefined;
352
+ const replacementWorkerReady = createWorkerReadyCallback(resolvedOptions.timeoutMs, userWarmup);
253
353
 
254
354
  // Create pooled transport with ProcessIO workers
255
355
  const transport = new PooledTransport({
256
- createTransport: () =>
356
+ createTransport: (): ProcessIO =>
257
357
  new ProcessIO({
258
358
  pythonPath: resolvedOptions.pythonPath,
259
359
  bridgeScript: resolvedOptions.scriptPath,
@@ -264,7 +364,8 @@ export class NodeBridge extends BridgeProtocol {
264
364
  minWorkers: resolvedOptions.minProcesses,
265
365
  queueTimeoutMs: resolvedOptions.queueTimeoutMs,
266
366
  maxConcurrentPerWorker: resolvedOptions.maxConcurrentPerProcess,
267
- onWorkerReady,
367
+ onWorkerReady: userWarmup,
368
+ onReplacementWorkerReady: replacementWorkerReady,
268
369
  });
269
370
 
270
371
  // Initialize BridgeProtocol with pooled transport
@@ -460,8 +561,8 @@ export class NodeBridge extends BridgeProtocol {
460
561
  * Simple request ID generator for warmup commands.
461
562
  */
462
563
  let warmupRequestId = 0;
463
- function generateWarmupId(): string {
464
- return `warmup_${++warmupRequestId}_${Date.now()}`;
564
+ function generateWarmupId(): number {
565
+ return ++warmupRequestId;
465
566
  }
466
567
 
467
568
  /**
@@ -471,38 +572,156 @@ function generateWarmupId(): string {
471
572
  * bypassing the pool to ensure each worker gets warmed up individually.
472
573
  */
473
574
  function createWarmupCallback(
474
- warmupCommands: Array<
475
- | { module: string; functionName: string; args?: unknown[] }
476
- | { method: string; params: unknown }
477
- >,
575
+ warmupCommands: WarmupCommand[],
478
576
  timeoutMs: number
479
577
  ): (worker: PooledWorker) => Promise<void> {
480
578
  return async (worker: PooledWorker) => {
481
- for (const cmd of warmupCommands) {
482
- try {
483
- // Handle both new and legacy warmup command formats
484
- if ('module' in cmd && 'functionName' in cmd) {
485
- // Build the protocol message
486
- const message = JSON.stringify({
487
- id: generateWarmupId(),
488
- type: 'call',
489
- module: cmd.module,
490
- functionName: cmd.functionName,
491
- args: cmd.args ?? [],
492
- kwargs: {},
493
- });
494
-
495
- // Send directly to this worker's transport
496
- await worker.transport.send(message, timeoutMs);
579
+ for (const [index, cmd] of warmupCommands.entries()) {
580
+ const commandLabel = `${cmd.module}.${cmd.functionName}`;
581
+ await executeWorkerCall(
582
+ worker,
583
+ {
584
+ module: cmd.module,
585
+ functionName: cmd.functionName,
586
+ args: cmd.args ?? [],
587
+ },
588
+ timeoutMs,
589
+ {
590
+ label: `Warmup command #${index + 1} (${commandLabel})`,
591
+ invalidJsonMessage: 'returned invalid JSON response',
592
+ malformedEnvelopeMessage: requestId =>
593
+ `returned malformed response envelope for request ${requestId}`,
594
+ pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
595
+ malformedErrorPayloadMessage: 'failed with malformed error payload',
497
596
  }
498
- // Legacy format { method, params } is ignored as it's not supported
499
- } catch {
500
- // Ignore warmup errors - they're not critical
597
+ );
598
+ }
599
+ };
600
+ }
601
+
602
+ function createWorkerReadyCallback(
603
+ timeoutMs: number,
604
+ extraWarmup?: (worker: PooledWorker) => Promise<void>
605
+ ): (worker: PooledWorker) => Promise<void> {
606
+ return async (worker: PooledWorker) => {
607
+ const result = await executeWorkerCall(
608
+ worker,
609
+ {
610
+ module: 'builtins',
611
+ functionName: 'len',
612
+ args: [[]],
613
+ },
614
+ Math.max(timeoutMs, WORKER_READY_TIMEOUT_MS),
615
+ {
616
+ label: 'Worker readiness probe (builtins.len)',
617
+ invalidJsonMessage: 'returned invalid JSON response',
618
+ malformedEnvelopeMessage: requestId =>
619
+ `returned malformed response envelope for request ${requestId}`,
620
+ pythonErrorMessage: (errorType, errorMessage) => `failed: ${errorType}: ${errorMessage}`,
621
+ malformedErrorPayloadMessage: 'failed with malformed error payload',
501
622
  }
623
+ );
624
+
625
+ if (result !== 0) {
626
+ throw new BridgeExecutionError(
627
+ `Worker readiness probe (builtins.len) returned unexpected result: ${JSON.stringify(result)}`
628
+ );
502
629
  }
630
+
631
+ await extraWarmup?.(worker);
503
632
  };
504
633
  }
505
634
 
635
+ interface WorkerCallSpec {
636
+ module: string;
637
+ functionName: string;
638
+ args: unknown[];
639
+ }
640
+
641
+ interface WorkerCallErrorMessages {
642
+ label: string;
643
+ invalidJsonMessage: string;
644
+ malformedEnvelopeMessage: (requestId: number) => string;
645
+ pythonErrorMessage: (errorType: string, errorMessage: string) => string;
646
+ malformedErrorPayloadMessage: string;
647
+ }
648
+
649
+ async function executeWorkerCall(
650
+ worker: PooledWorker,
651
+ request: WorkerCallSpec,
652
+ timeoutMs: number,
653
+ messages: WorkerCallErrorMessages
654
+ ): Promise<unknown> {
655
+ const requestId = generateWarmupId();
656
+ let message: string;
657
+ try {
658
+ message = JSON.stringify({
659
+ id: requestId,
660
+ protocol: PROTOCOL_ID,
661
+ method: 'call',
662
+ params: {
663
+ module: request.module,
664
+ functionName: request.functionName,
665
+ args: request.args,
666
+ kwargs: {},
667
+ },
668
+ });
669
+ } catch (error) {
670
+ throw new BridgeExecutionError(
671
+ `${messages.label} failed to encode request: ${error instanceof Error ? error.message : String(error)}`,
672
+ { cause: error instanceof Error ? error : undefined }
673
+ );
674
+ }
675
+
676
+ let response: string;
677
+ try {
678
+ response = await worker.transport.send(message, timeoutMs);
679
+ } catch (error) {
680
+ throw new BridgeExecutionError(
681
+ `${messages.label} failed to send: ${error instanceof Error ? error.message : String(error)}`,
682
+ { cause: error instanceof Error ? error : undefined }
683
+ );
684
+ }
685
+
686
+ let parsed: unknown;
687
+ try {
688
+ parsed = JSON.parse(response);
689
+ } catch (error) {
690
+ throw new BridgeExecutionError(`${messages.label} ${messages.invalidJsonMessage}`, {
691
+ cause: error instanceof Error ? error : undefined,
692
+ });
693
+ }
694
+
695
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
696
+ throw new BridgeExecutionError(
697
+ `${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
698
+ );
699
+ }
700
+
701
+ const envelope = parsed as { result?: unknown; error?: unknown };
702
+ if ('error' in envelope && envelope.error !== undefined && envelope.error !== null) {
703
+ const errorPayload = envelope.error;
704
+ if (errorPayload && typeof errorPayload === 'object' && !Array.isArray(errorPayload)) {
705
+ const err = errorPayload as { type?: unknown; message?: unknown };
706
+ const errType = typeof err.type === 'string' ? err.type : 'Error';
707
+ const errMessage = typeof err.message === 'string' ? err.message : 'Unknown error';
708
+ throw new BridgeExecutionError(
709
+ `${messages.label} ${messages.pythonErrorMessage(errType, errMessage)}`
710
+ );
711
+ }
712
+
713
+ throw new BridgeExecutionError(`${messages.label} ${messages.malformedErrorPayloadMessage}`);
714
+ }
715
+
716
+ if (!('result' in envelope)) {
717
+ throw new BridgeExecutionError(
718
+ `${messages.label} ${messages.malformedEnvelopeMessage(requestId)}`
719
+ );
720
+ }
721
+
722
+ return envelope.result;
723
+ }
724
+
506
725
  // =============================================================================
507
726
  // ENVIRONMENT BUILDING
508
727
  // =============================================================================
@@ -513,13 +732,13 @@ function createWarmupCallback(
513
732
  function buildProcessEnv(options: ResolvedOptions): Record<string, string> {
514
733
  const allowedPrefixes = ['TYWRAP_'];
515
734
  const allowedKeys = new Set(['path', 'pythonpath', 'virtual_env', 'pythonhome']);
516
- const env: Record<string, string> = {};
735
+ const env = createNullPrototypeEnv();
517
736
 
518
737
  // Copy allowed env vars from process.env
519
738
  if (options.inheritProcessEnv) {
520
739
  for (const [key, value] of Object.entries(process.env)) {
521
740
  if (value !== undefined) {
522
- env[key] = value;
741
+ setEnvValue(env, key, value);
523
742
  }
524
743
  }
525
744
  } else {
@@ -528,15 +747,16 @@ function buildProcessEnv(options: ResolvedOptions): Record<string, string> {
528
747
  value !== undefined &&
529
748
  (allowedKeys.has(key.toLowerCase()) || allowedPrefixes.some(p => key.startsWith(p)))
530
749
  ) {
531
- env[key] = value;
750
+ setEnvValue(env, key, value);
532
751
  }
533
752
  }
534
753
  }
535
754
 
536
755
  // Apply user overrides
537
756
  for (const [key, value] of Object.entries(options.env)) {
757
+ assertSafeEnvOverrideKey(key);
538
758
  if (value !== undefined) {
539
- env[key] = value;
759
+ setEnvValue(env, key, value);
540
760
  }
541
761
  }
542
762
 
@@ -545,8 +765,8 @@ function buildProcessEnv(options: ResolvedOptions): Record<string, string> {
545
765
  const venv = resolveVirtualEnv(options.virtualEnv, options.cwd);
546
766
  env.VIRTUAL_ENV = venv.venvPath;
547
767
  const pathKey = getPathKey(env);
548
- const currentPath = env[pathKey] ?? '';
549
- env[pathKey] = `${venv.binDir}${delimiter}${currentPath}`;
768
+ const currentPath = getEnvValue(env, pathKey);
769
+ setPathValue(env, currentPath ? `${venv.binDir}${delimiter}${currentPath}` : venv.binDir);
550
770
  }
551
771
 
552
772
  // Add cwd to PYTHONPATH so Python can find modules in the working directory
@@ -18,4 +18,7 @@
18
18
  *
19
19
  * This file is maintained for backward compatibility only.
20
20
  */
21
- export { NodeBridge as OptimizedNodeBridge, type NodeBridgeOptions as ProcessPoolOptions } from './node.js';
21
+ export {
22
+ NodeBridge as OptimizedNodeBridge,
23
+ type NodeBridgeOptions as ProcessPoolOptions,
24
+ } from './node.js';
@@ -41,6 +41,11 @@ export interface PooledTransportOptions {
41
41
  * Use this for per-worker warmup (e.g., importing modules, running setup).
42
42
  */
43
43
  onWorkerReady?: (worker: PooledWorker) => Promise<void>;
44
+
45
+ /**
46
+ * Optional callback used only for background replacement workers.
47
+ */
48
+ onReplacementWorkerReady?: (worker: PooledWorker) => Promise<void>;
44
49
  }
45
50
 
46
51
  // =============================================================================
@@ -78,8 +83,12 @@ export interface PooledTransportOptions {
78
83
  * ```
79
84
  */
80
85
  export class PooledTransport extends BoundedContext implements Transport {
81
- private readonly poolOptions: Omit<Required<PooledTransportOptions>, 'onWorkerReady'> & {
86
+ private readonly poolOptions: Omit<
87
+ Required<PooledTransportOptions>,
88
+ 'onWorkerReady' | 'onReplacementWorkerReady'
89
+ > & {
82
90
  onWorkerReady?: (worker: PooledWorker) => Promise<void>;
91
+ onReplacementWorkerReady?: (worker: PooledWorker) => Promise<void>;
83
92
  };
84
93
  private pool?: WorkerPool;
85
94
 
@@ -102,6 +111,7 @@ export class PooledTransport extends BoundedContext implements Transport {
102
111
  queueTimeoutMs: options.queueTimeoutMs ?? 30000,
103
112
  maxConcurrentPerWorker: options.maxConcurrentPerWorker ?? 10,
104
113
  onWorkerReady: options.onWorkerReady,
114
+ onReplacementWorkerReady: options.onReplacementWorkerReady,
105
115
  };
106
116
  }
107
117
 
@@ -123,6 +133,7 @@ export class PooledTransport extends BoundedContext implements Transport {
123
133
  queueTimeoutMs: this.poolOptions.queueTimeoutMs,
124
134
  maxConcurrentPerWorker: this.poolOptions.maxConcurrentPerWorker,
125
135
  onWorkerReady: this.poolOptions.onWorkerReady,
136
+ onReplacementWorkerReady: this.poolOptions.onReplacementWorkerReady,
126
137
  });
127
138
 
128
139
  await this.pool.init();