@pulse-compute/wasm-host-runtime 1.0.0-beta.4 → 1.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,9 +7,9 @@
7
7
  > **Supported entry points:** `None for application authors.`<br>
8
8
  > **Stability:** Internal compiler/runtime interface synchronized with this release set.<br>
9
9
  > **npm:** [`@pulse-compute/wasm-host-runtime`](https://www.npmjs.com/package/@pulse-compute/wasm-host-runtime)<br>
10
- > **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.4/packages/implementation-packages/)
10
+ > **Canonical documentation:** [Package guide](https://pulsecompute.io/v1.0.0-beta.5/packages/implementation-packages/)
11
11
  >
12
- > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.4` package policy.
12
+ > This release-status block is generated from the synchronized `Pulse 1.0.0-beta.5` package policy.
13
13
  <!-- pulse-package-status:end -->
14
14
 
15
15
  Package-owned PulseWasm host-runtime build-time builders and local runtime helpers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pulse-compute/wasm-host-runtime",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "license": "Apache-2.0",
5
5
  "engines": {
6
6
  "node": "^22.14.0 || ^24.0.0"
@@ -28,11 +28,11 @@
28
28
  "./runtime/canonical-native-host": "./src/runtime/canonical-native-host.js"
29
29
  },
30
30
  "dependencies": {
31
- "@pulse-compute/crypto": "1.0.0-beta.4",
32
- "@pulse-compute/runtime": "1.0.0-beta.4",
33
- "@pulse-compute/wasm-build-support": "1.0.0-beta.4",
34
- "@pulse-compute/wasm-contracts": "1.0.0-beta.4",
35
- "@pulse-compute/wasm-schema-json": "1.0.0-beta.4"
31
+ "@pulse-compute/crypto": "1.0.0-beta.5",
32
+ "@pulse-compute/runtime": "1.0.0-beta.5",
33
+ "@pulse-compute/wasm-build-support": "1.0.0-beta.5",
34
+ "@pulse-compute/wasm-contracts": "1.0.0-beta.5",
35
+ "@pulse-compute/wasm-schema-json": "1.0.0-beta.5"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
@@ -48,7 +48,7 @@
48
48
  "url": "git+https://github.com/pulse-compute/pulse.git",
49
49
  "directory": "wasm/packages/host-runtime"
50
50
  },
51
- "homepage": "https://pulsecompute.io/v1.0.0-beta.4/packages/implementation-packages/",
51
+ "homepage": "https://pulsecompute.io/v1.0.0-beta.5/packages/implementation-packages/",
52
52
  "bugs": {
53
53
  "url": "https://github.com/pulse-compute/pulse/issues"
54
54
  }
@@ -390,6 +390,7 @@ function assertSchemaBodySize(schemaCodecs, text, schemaId, source) {
390
390
  const runtimeValueErrorCodes = Object.freeze({
391
391
  BodyTooLargeError: 'PULSE_BODY_TOO_LARGE',
392
392
  BodyDecodeError: 'PULSE_BODY_DECODE',
393
+ RequestBodyInvalidUtf8Error: 'PULSE_REQUEST_BODY_INVALID_UTF8',
393
394
  BodyUnavailableError: 'PULSE_BODY_UNAVAILABLE',
394
395
  OpaqueBodyInspectionError: 'PULSE_OPAQUE_BODY_INSPECTION',
395
396
  ResponseEncodeError: 'PULSE_RESPONSE_ENCODE',
@@ -431,7 +432,7 @@ function normalizeRequest(options = {}) {
431
432
  function createRequestSurface(request, options = {}, schemaCodecs, trace) {
432
433
  const body = runtimeApi.body.createValue(
433
434
  { body: request.body, headers: request.headers },
434
- { maxBytes: options.maxRequestBodyBytes || options.maxBodyBytes || options.maxStructuredBodyBytes }
435
+ { maxBytes: options.maxRequestBodyBytes || options.maxBodyBytes || options.maxStructuredBodyBytes, strictUtf8: true }
435
436
  );
436
437
  const schemaMemo = new Map();
437
438
  return Object.freeze({
@@ -634,6 +635,30 @@ function createCanonicalContext(options, trace) {
634
635
  }
635
636
 
636
637
  const ctx = Object.freeze({
638
+ decodeJson(text, schemaId) {
639
+ const key = requireSchemaId(schemaCodecs, schemaId, 'application-text');
640
+ const event = {
641
+ boundary: 'application-text', schemaId: key, responseCaseId: null,
642
+ operationId: `application-text:${key}`, status: null,
643
+ contentType: null, headers: [], bodyOwnership: 'application-owned',
644
+ target: options.target || null, provider: options.provider || null,
645
+ effectId: null, groupId: null
646
+ };
647
+ try {
648
+ if (typeof text !== 'string') {
649
+ throw new CanonicalRuntimeError('SchemaDecodeError', 'PULSE_SCHEMA_DECODE',
650
+ 'Application JSON text must be a string.',
651
+ { schemaId: key, source: 'application-text', expected: 'string', actualKind: typeof text });
652
+ }
653
+ assertSchemaBodySize(schemaCodecs, text, key, 'application-text');
654
+ const value = schemaCodecs.decodeJsonText(key, text, 'application-text');
655
+ jsonTrace(trace, { ...event, kind: 'json.decode.text' }, value);
656
+ return value;
657
+ } catch (error) {
658
+ jsonTrace(trace, { ...event, kind: 'json.decode.error', ...jsonTraceErrorDetail(error) });
659
+ throw error;
660
+ }
661
+ },
637
662
  encodeJson(value, schemaId) {
638
663
  const key = requireSchemaId(schemaCodecs, schemaId, 'application-value');
639
664
  const event = {
@@ -911,6 +936,7 @@ function normalizeProviderEffect(effect, schemaCodecs, options = {}) {
911
936
  if (effect.kind === 'fetch') return normalizeFetchEffect(effect, schemaCodecs, options);
912
937
  const id = String(effect.id || '');
913
938
  if (!id) throw new CanonicalRuntimeError('CanonicalEffectProtocolError', 'PULSE_CANONICAL_EFFECT_PROTOCOL', 'Canonical provider effect requires an id.', { effect });
939
+ if (effect.kind === 'time.now') return Object.freeze({ id, kind: 'time.now', providerKind: 'time', operation: 'now', capability: 'time.wall-clock', source: effect.source, ...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }) });
914
940
  if (effect.kind === 'config.get' || effect.kind === 'secret.get') {
915
941
  return Object.freeze({ id, kind: effect.kind, name: String(effect.name), ...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }), source: effect.source });
916
942
  }
@@ -1324,6 +1350,16 @@ function createCanonicalHostRuntime(options = {}) {
1324
1350
  const registry = options.continuationRegistry || createContinuationRegistry({ ttlMs: options.continuationTtlMs, clock: options.clock });
1325
1351
 
1326
1352
  async function execute(programModule, executionOptions = {}) {
1353
+ const input = { ...options, ...executionOptions };
1354
+ const budget = portableKv.createRequestBudget(input);
1355
+ try {
1356
+ budget.check();
1357
+ return await executeInvocation(programModule, { ...executionOptions, requestBudget: budget, signal: budget.signal,
1358
+ deadlineMonotonicMs: budget.deadlineMonotonicMs ?? input.deadlineMonotonicMs, kvClock: budget.deadlineMonotonicMs === undefined ? input.kvClock : budget.clock });
1359
+ } finally { if (!input.requestBudget) budget.close(); }
1360
+ }
1361
+
1362
+ async function executeInvocation(programModule, executionOptions) {
1327
1363
  if (!programModule || typeof programModule.createHandler !== 'function') throw new TypeError('Canonical runtime requires a compiled canonical module.');
1328
1364
  const metadata = assertCanonicalProgramCompatibility(programModule);
1329
1365
  executionSequence += 1;
@@ -1385,6 +1421,7 @@ function createCanonicalHostRuntime(options = {}) {
1385
1421
  }
1386
1422
 
1387
1423
  async function dispatchOne(effect) {
1424
+ executionOptions.requestBudget.check();
1388
1425
  const normalized = normalizeProviderEffect(effect, schemaCodecs, runtimeOptions);
1389
1426
  const conditional = portableKv.isConditionalKv(normalized.kind);
1390
1427
  if (conditional) portableKv.registerKvRedactions(normalized, (value) => sensitiveValues.add(value));
@@ -1394,7 +1431,8 @@ function createCanonicalHostRuntime(options = {}) {
1394
1431
  const effectExecution = { ...options, ...executionOptions, metadata, executionId, trace: traceSink, registerRedactionValue: (value) => sensitiveValues.add(value), onKvObservation: recordTrace };
1395
1432
  let snapshot = conditional
1396
1433
  ? await portableKv.executeConditionalKv(normalized, adapter.prepareConditionalKv || ((_admitted, kvExecution) => () => adapter.dispatchEffect(normalized, kvExecution)), effectExecution, runtimeOptions)
1397
- : await adapter.dispatchEffect(normalized, effectExecution);
1434
+ : await executionOptions.requestBudget.race(adapter.dispatchEffect(normalized, effectExecution));
1435
+ executionOptions.requestBudget.check();
1398
1436
  if (normalized.kind === 'config.get' || normalized.kind === 'secret.get') {
1399
1437
  if (snapshot !== undefined && typeof snapshot !== 'string') {
1400
1438
  throw new CanonicalRuntimeError(
@@ -1415,6 +1453,7 @@ function createCanonicalHostRuntime(options = {}) {
1415
1453
  { receivedType: Array.isArray(snapshot) ? 'array' : snapshot === null ? 'null' : typeof snapshot }
1416
1454
  );
1417
1455
  }
1456
+ if (normalized.kind === 'time.now') snapshot = require('@pulse-compute/runtime/host').normalizeTimeResult(snapshot);
1418
1457
  if (normalized.kind === 'event.emit' && snapshot !== undefined) {
1419
1458
  throw new CanonicalRuntimeError(
1420
1459
  'EventAcceptanceError',
@@ -1487,7 +1526,7 @@ function createCanonicalHostRuntime(options = {}) {
1487
1526
  const generator = programModule.createHandler()(context.ctx, pulse);
1488
1527
  if (!generator || typeof generator.next !== 'function') throw new CanonicalRuntimeError('CanonicalHandlerProtocolError', 'PULSE_CANONICAL_HANDLER_PROTOCOL', 'Compiled canonical handler did not return a generator.');
1489
1528
  let step;
1490
- try { step = generator.next(); }
1529
+ try { executionOptions.requestBudget.check(); step = generator.next(); }
1491
1530
  catch (error) { error.execution = { executionId, trace: [...trace], continuations: executionContinuations() }; throw error; }
1492
1531
 
1493
1532
  while (!step.done) {
@@ -1501,6 +1540,7 @@ function createCanonicalHostRuntime(options = {}) {
1501
1540
  let value;
1502
1541
  try {
1503
1542
  value = marker.kind === 'group' ? await dispatchGroup(effects) : await dispatchOne(effects[0]);
1543
+ executionOptions.requestBudget.check();
1504
1544
  registry.resume(continuationId);
1505
1545
  recordTrace(Object.freeze({ type: 'continuation-resumed', provider: adapter.id, executionId, continuationId, branchPoint: marker.continuationId }));
1506
1546
  step = generator.next(value);
@@ -1517,7 +1557,7 @@ function createCanonicalHostRuntime(options = {}) {
1517
1557
  }
1518
1558
 
1519
1559
  let response;
1520
- try { response = finalResponse(step.value, context.request.method); }
1560
+ try { executionOptions.requestBudget.check(); response = finalResponse(step.value, context.request.method); }
1521
1561
  catch (error) {
1522
1562
  error.execution = Object.freeze({ executionId, trace: Object.freeze([...trace]), continuations: Object.freeze(executionContinuations()), resolutionOrder: Object.freeze([...resolutionOrder]) });
1523
1563
  throw error;
@@ -1570,6 +1610,7 @@ module.exports = {
1570
1610
  CanonicalRuntimeError,
1571
1611
  assertCanonicalProgramCompatibility,
1572
1612
  createCanonicalHostRuntime,
1613
+ createRequestBudget: portableKv.createRequestBudget,
1573
1614
  executeCanonicalProgram,
1574
1615
  createCanonicalContext,
1575
1616
  normalizeFetchResponse,
@@ -244,6 +244,7 @@ function nativeEffect(planEffect, payload) {
244
244
  resource: planEffect.resource,
245
245
  source: planEffect.source
246
246
  };
247
+ if (planEffect.kind === 'time.now') return { ...base, providerKind: 'time', operation: 'now', capability: 'time.wall-clock' };
247
248
  if (planEffect.kind === 'fetch') {
248
249
  const response = nativeFetchResponseContract(planEffect, payload);
249
250
  return { ...base, url: payload.url, init: payload.init, ...response };
@@ -631,6 +632,11 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
631
632
  return put(unary(operator, value(valueHandle)));
632
633
  },
633
634
  value_truthy(valueHandle) { return value(valueHandle) ? 1 : 0; },
635
+ value_string_trim(valueHandle) {
636
+ const input = value(valueHandle);
637
+ if (typeof input !== 'string') throw new CanonicalNativeHostError('String trim requires a string value.', 'PULSE_CANONICAL_NATIVE_VALUE_TYPE');
638
+ return put(input.trim());
639
+ },
634
640
  value_nullish(valueHandle) { return value(valueHandle) === null || value(valueHandle) === undefined ? 1 : 0; },
635
641
  log(level, messageHandle) {
636
642
  const methods = Object.freeze({ 1: 'error', 2: 'warn', 3: 'info', 4: 'debug' });
@@ -645,7 +651,12 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
645
651
  request_url() { requireHttpSurface('ctx.req.url'); return put(context.ctx.req.url); },
646
652
  request_path() { requireHttpSurface('ctx.req.path'); return put(context.ctx.req.path); },
647
653
  request_headers() { requireHttpSurface('ctx.req.headers'); return put(context.ctx.req.headers); },
648
- request_header(nameHandle) { requireHttpSurface('ctx.req.header'); return put(context.ctx.req.header(value(nameHandle))); },
654
+ request_header(nameHandle) {
655
+ requireHttpSurface('ctx.req.header');
656
+ const name = String(value(nameHandle)).toLowerCase();
657
+ const header = context.ctx.req.headers.find(([key]) => String(key).toLowerCase() === name);
658
+ return put(header ? header[1] : undefined);
659
+ },
649
660
  request_text() { requireHttpSurface('ctx.req.text'); return put(context.ctx.req.text()); },
650
661
  request_json(schemaHandle) {
651
662
  requireHttpSurface('ctx.req.json');
@@ -656,6 +667,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
656
667
  router_param(pathHandle, patternHandle, nameHandle) { return put(routerParam(value(pathHandle), value(patternHandle), value(nameHandle))); },
657
668
  response_json(valueHandle, optionsHandle) { requireHttpSurface('ctx.json'); return put(context.ctx.json(value(valueHandle), value(optionsHandle))); },
658
669
  schema_encode(valueHandle, schemaHandle) { return put(context.ctx.encodeJson(value(valueHandle), value(schemaHandle))); },
670
+ schema_decode(textHandle, schemaHandle) { return put(context.ctx.decodeJson(value(textHandle), value(schemaHandle))); },
659
671
  response_text(valueHandle, optionsHandle) { requireHttpSurface('ctx.text'); return put(context.ctx.text(value(valueHandle), value(optionsHandle))); },
660
672
  response_custom(valueHandle) { requireHttpSurface('ctx.response'); return put(context.ctx.response(value(valueHandle))); },
661
673
  grip_is_websocket() {
@@ -748,6 +760,24 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
748
760
  else env[item.name] = () => { throw new CanonicalNativeHostError(`Unsupported AssemblyScript env import ${item.name}.`, 'PULSE_CANONICAL_NATIVE_ENV_IMPORT_UNSUPPORTED', { name: item.name }); };
749
761
  }
750
762
 
763
+ let applicationError = 0;
764
+ const applicationErrors = executionPlane === 'http' && (plan.routing?.entries || []).some(entry => entry.kind === 'error');
765
+ function captureApplicationError(error) {
766
+ if (!applicationErrors || options.signal?.aborted || !portableKv.isApplicationError(error)) return false;
767
+ if (!applicationError) {
768
+ const safe = canonicalRuntime.redactRuntimeError(error, sensitiveValues);
769
+ applicationError = put(Object.freeze({ name: safe.name, code: safe.code, message: safe.message }));
770
+ }
771
+ return true;
772
+ }
773
+ if (applicationErrors) for (const [name, implementation] of Object.entries(pulseHost)) {
774
+ pulseHost[name] = (...args) => {
775
+ if (applicationError) return 0;
776
+ try { return implementation(...args); }
777
+ catch (error) { if (!captureApplicationError(error)) throw error; return 0; }
778
+ };
779
+ }
780
+ pulseHost.router_error_take = () => { const error = applicationError; applicationError = 0; return error; };
751
781
  instance = new WebAssembly.Instance(module, { pulse_host: pulseHost, env });
752
782
  if (schemaIndexes.size > 0) {
753
783
  const available = new Set(moduleShape.exports.map((entry) => entry.name));
@@ -790,6 +820,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
790
820
  }
791
821
 
792
822
  let result = portableKv.isConditionalKv(effect.kind) ? portableKv.normalizeConditionalKvResult(effect, rawResult, options) : rawResult;
823
+ if (effect.kind === 'time.now') result = require('@pulse-compute/runtime/host').normalizeTimeResult(rawResult);
793
824
  if (effect.kind === 'event.emit' && result !== undefined) {
794
825
  throw new canonicalRuntime.CanonicalRuntimeError(
795
826
  'EventAcceptanceError',
@@ -897,6 +928,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
897
928
  resume() { return instance.exports.pulse_resume(); },
898
929
  pendingEffects: takePending,
899
930
  prepareEffectResult,
931
+ captureApplicationError,
900
932
  setEffectResult,
901
933
  resultValue,
902
934
  response() { requireHttpSurface('HTTP completion'); return canonicalRuntime.finalResponse(resultValue(), context.ctx.req.method); },
@@ -915,9 +947,18 @@ function nativeEventCancelled(message) {
915
947
  return new CanonicalNativeHostError(message, 'PULSE_CANONICAL_NATIVE_EVENT_CANCELLED');
916
948
  }
917
949
 
918
- function raceNativeEventSignal(value, signal) {
950
+ function nativeRequestCancelled(signal) {
951
+ if (['PULSE_REQUEST_DEADLINE_EXCEEDED', 'PULSE_REQUEST_CLOCK_INVALID'].includes(signal.reason?.code)) return signal.reason;
952
+ return new portableKv.PulseRuntimeContractError('PULSE_RUNTIME_EFFECT_ABORTED',
953
+ signal.reason instanceof Error ? signal.reason.message : 'Native execution was cancelled.',
954
+ { cause: signal.reason });
955
+ }
956
+
957
+ function raceNativeSignal(value, signal, eventMode = true) {
919
958
  if (!signal) return Promise.resolve(value);
920
- if (signal.aborted) return Promise.reject(nativeEventCancelled('Native event execution was cancelled during effect dispatch.'));
959
+ const cancelled = () => eventMode ? nativeEventCancelled('Native event execution was cancelled during effect dispatch.')
960
+ : nativeRequestCancelled(signal);
961
+ if (signal.aborted) return Promise.reject(cancelled());
921
962
  return new Promise((resolve, reject) => {
922
963
  let settled = false;
923
964
  const finish = (callback, result) => {
@@ -926,7 +967,7 @@ function raceNativeEventSignal(value, signal) {
926
967
  signal.removeEventListener('abort', onAbort);
927
968
  callback(result);
928
969
  };
929
- const onAbort = () => finish(reject, nativeEventCancelled('Native event execution was cancelled during effect dispatch.'));
970
+ const onAbort = () => finish(reject, cancelled());
930
971
  signal.addEventListener('abort', onAbort, { once: true });
931
972
  Promise.resolve(value).then(
932
973
  (result) => finish(resolve, result),
@@ -955,14 +996,17 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
955
996
  let executionFailure;
956
997
  let activeContinuation;
957
998
  try {
999
+ if (!eventMode && options.signal?.aborted) throw nativeRequestCancelled(options.signal);
958
1000
  if (eventMode && options.signal && options.signal.aborted) {
959
1001
  throw nativeEventCancelled('Native event execution was cancelled before handler entry.');
960
1002
  }
1003
+ options.requestBudget?.check();
961
1004
  status = eventMode ? controller.startEvent(invocation.frame) : controller.start();
962
1005
  while (status === runtimeContract.CANONICAL_NATIVE_RUN_STATUS.SUSPENDED) {
963
1006
  if (eventMode && options.signal && options.signal.aborted) {
964
1007
  throw nativeEventCancelled('Native event execution was cancelled while suspended.');
965
1008
  }
1009
+ options.requestBudget?.check();
966
1010
  const pending = controller.pendingEffects();
967
1011
  if (pending.length !== controller.exports.pulse_pending_count()) {
968
1012
  throw new CanonicalNativeHostError('Native pending-effect count differs from host queue.', 'PULSE_CANONICAL_NATIVE_PENDING_MISMATCH', { moduleCount: controller.exports.pulse_pending_count(), hostCount: pending.length });
@@ -995,7 +1039,8 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
995
1039
  }
996
1040
  effectCount += pending.length;
997
1041
 
998
- const settled = await Promise.allSettled(pending.map(async (entry) => {
1042
+ const settled = await raceNativeSignal(Promise.allSettled(pending.map(async (entry) => {
1043
+ options.requestBudget?.check();
999
1044
  if (eventMode && options.signal && options.signal.aborted) {
1000
1045
  throw nativeEventCancelled('Native event execution was cancelled before effect dispatch.');
1001
1046
  }
@@ -1033,19 +1078,27 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
1033
1078
  const rawResult = conditional
1034
1079
  ? await portableKv.executeConditionalKv(normalized, adapter.prepareConditionalKv || ((_admitted, kvExecution) => () => adapter.dispatchEffect(normalized, kvExecution)),
1035
1080
  { ...effectExecution, onKvObservation: (event) => controller.trace.push(event) }, options)
1036
- : await raceNativeEventSignal(adapter.dispatchEffect(normalized, effectExecution), eventMode ? options.signal : undefined);
1081
+ : await raceNativeSignal(adapter.dispatchEffect(normalized, effectExecution), options.signal, eventMode);
1082
+ options.requestBudget?.check();
1037
1083
  const result = controller.prepareEffectResult(entry.index, rawResult);
1038
1084
  controller.trace.push(Object.freeze(redactValue({ type: 'native-effect-resolved', executionId, provider: adapter.id, effectId: normalized.id, kind: normalized.kind, result: privateEffect || entry.effect.kind === 'secret.get' ? '<redacted>' : result && typeof result.toJSON === 'function' ? result.toJSON() : result }, controller.sensitiveValues)));
1039
1085
  resolutionOrder.push(entry.effect.id);
1040
1086
  return { entry, result };
1041
- }));
1087
+ })), options.signal, eventMode);
1042
1088
 
1043
1089
  if (eventMode && options.signal && options.signal.aborted) {
1044
1090
  throw nativeEventCancelled('Native event execution was cancelled while suspended.');
1045
1091
  }
1046
- const failed = settled.find((entry) => entry.status === 'rejected');
1047
- if (failed) throw canonicalRuntime.redactRuntimeError(failed.reason, controller.sensitiveValues);
1048
- for (const item of settled) controller.setEffectResult(item.value.entry.index, item.value.result);
1092
+ options.requestBudget?.check();
1093
+ if (!eventMode && options.signal?.aborted) throw nativeRequestCancelled(options.signal);
1094
+ const failures = settled.filter(entry => entry.status === 'rejected');
1095
+ const failed = failures.find(entry => !portableKv.isApplicationError(entry.reason)) || failures[0];
1096
+ if (failed && (!failures.every(entry => portableKv.isApplicationError(entry.reason))
1097
+ || !controller.captureApplicationError(failed.reason))) {
1098
+ throw canonicalRuntime.redactRuntimeError(failed.reason, controller.sensitiveValues);
1099
+ }
1100
+ settled.forEach((item, index) => controller.setEffectResult(pending[index].index,
1101
+ item.status === 'fulfilled' ? item.value.result : undefined));
1049
1102
  const previousPc = controller.programCounter();
1050
1103
  activeContinuation.state = 'resumed';
1051
1104
  activeContinuation.states.push('resumed');
@@ -1053,8 +1106,8 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
1053
1106
  if (status === runtimeContract.CANONICAL_NATIVE_RUN_STATUS.INVALID_RESUME) {
1054
1107
  throw new CanonicalNativeHostError('Native module rejected a complete effect-result set.', 'PULSE_CANONICAL_NATIVE_INVALID_RESUME', { programCounter: previousPc, errorCode: controller.lastErrorCode() });
1055
1108
  }
1056
- activeContinuation.state = 'completed';
1057
- activeContinuation.states.push('completed');
1109
+ activeContinuation.state = failed ? 'failed' : 'completed';
1110
+ activeContinuation.states.push(activeContinuation.state);
1058
1111
  controller.trace.push(Object.freeze(redactValue({ type: 'native-continuation-resumed', executionId, continuationId: continuation.id, continuationState }, controller.sensitiveValues)));
1059
1112
  activeContinuation = undefined;
1060
1113
  }
@@ -1086,6 +1139,8 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
1086
1139
  valueHandleCount: controller.heap.size()
1087
1140
  });
1088
1141
  }
1142
+ if (options.signal?.aborted) throw nativeRequestCancelled(options.signal);
1143
+ options.requestBudget?.check();
1089
1144
  const response = controller.response();
1090
1145
  controller.trace.push(Object.freeze(redactValue({ type: 'native-execution-completed', executionId, provider: adapter.id, status: response.status, bodyClass: response.bodyClass }, controller.sensitiveValues)));
1091
1146
  return Object.freeze({
@@ -1126,7 +1181,11 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
1126
1181
  }
1127
1182
 
1128
1183
  async function executeCanonicalNativeModule(compiled, options = {}) {
1129
- return executeCanonicalNativeInvocation(compiled, options, Object.freeze({ plane: 'http' }));
1184
+ const budget = portableKv.createRequestBudget(options);
1185
+ try {
1186
+ budget.check();
1187
+ return await executeCanonicalNativeInvocation(compiled, { ...options, requestBudget: budget, signal: budget.signal, deadlineMonotonicMs: budget.deadlineMonotonicMs ?? options.deadlineMonotonicMs, kvClock: budget.deadlineMonotonicMs === undefined ? options.kvClock : budget.clock }, Object.freeze({ plane: 'http' }));
1188
+ } finally { if (!options.requestBudget) budget.close(); }
1130
1189
  }
1131
1190
 
1132
1191
  function nativeEventFailure(error) {