@pulse-compute/wasm-host-runtime 1.0.0-beta.3 → 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.3/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.3` 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.3",
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.3",
32
- "@pulse-compute/runtime": "1.0.0-beta.3",
33
- "@pulse-compute/wasm-build-support": "1.0.0-beta.3",
34
- "@pulse-compute/wasm-contracts": "1.0.0-beta.3",
35
- "@pulse-compute/wasm-schema-json": "1.0.0-beta.3"
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.3/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,50 @@ 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
+ },
662
+ encodeJson(value, schemaId) {
663
+ const key = requireSchemaId(schemaCodecs, schemaId, 'application-value');
664
+ const event = {
665
+ boundary: 'application-value', schemaId: key, responseCaseId: null,
666
+ operationId: `application-value:${key}`, status: null,
667
+ contentType: 'application/json; charset=utf-8', headers: [],
668
+ bodyOwnership: 'application-owned', target: options.target || null,
669
+ provider: options.provider || null, effectId: null, groupId: null
670
+ };
671
+ try {
672
+ const text = schemaCodecs.encodeJsonText(key, value, 'application-value');
673
+ assertSchemaBodySize(schemaCodecs, text, key, 'application-value');
674
+ const normalized = schemaCodecs.decodeJsonText(key, text, 'application-value-trace');
675
+ jsonTrace(trace, { ...event, kind: 'json.encode.value' }, normalized);
676
+ return text;
677
+ } catch (error) {
678
+ jsonTrace(trace, { ...event, kind: 'json.encode.error', ...jsonTraceErrorDetail(error) });
679
+ throw error;
680
+ }
681
+ },
637
682
  req: requestSurface.surface,
638
683
  log: Object.freeze(log),
639
684
  fetch() {
@@ -891,6 +936,7 @@ function normalizeProviderEffect(effect, schemaCodecs, options = {}) {
891
936
  if (effect.kind === 'fetch') return normalizeFetchEffect(effect, schemaCodecs, options);
892
937
  const id = String(effect.id || '');
893
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) }) });
894
940
  if (effect.kind === 'config.get' || effect.kind === 'secret.get') {
895
941
  return Object.freeze({ id, kind: effect.kind, name: String(effect.name), ...(effect.groupKey === undefined ? {} : { groupKey: String(effect.groupKey) }), source: effect.source });
896
942
  }
@@ -1304,6 +1350,16 @@ function createCanonicalHostRuntime(options = {}) {
1304
1350
  const registry = options.continuationRegistry || createContinuationRegistry({ ttlMs: options.continuationTtlMs, clock: options.clock });
1305
1351
 
1306
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) {
1307
1363
  if (!programModule || typeof programModule.createHandler !== 'function') throw new TypeError('Canonical runtime requires a compiled canonical module.');
1308
1364
  const metadata = assertCanonicalProgramCompatibility(programModule);
1309
1365
  executionSequence += 1;
@@ -1365,6 +1421,7 @@ function createCanonicalHostRuntime(options = {}) {
1365
1421
  }
1366
1422
 
1367
1423
  async function dispatchOne(effect) {
1424
+ executionOptions.requestBudget.check();
1368
1425
  const normalized = normalizeProviderEffect(effect, schemaCodecs, runtimeOptions);
1369
1426
  const conditional = portableKv.isConditionalKv(normalized.kind);
1370
1427
  if (conditional) portableKv.registerKvRedactions(normalized, (value) => sensitiveValues.add(value));
@@ -1374,7 +1431,8 @@ function createCanonicalHostRuntime(options = {}) {
1374
1431
  const effectExecution = { ...options, ...executionOptions, metadata, executionId, trace: traceSink, registerRedactionValue: (value) => sensitiveValues.add(value), onKvObservation: recordTrace };
1375
1432
  let snapshot = conditional
1376
1433
  ? await portableKv.executeConditionalKv(normalized, adapter.prepareConditionalKv || ((_admitted, kvExecution) => () => adapter.dispatchEffect(normalized, kvExecution)), effectExecution, runtimeOptions)
1377
- : await adapter.dispatchEffect(normalized, effectExecution);
1434
+ : await executionOptions.requestBudget.race(adapter.dispatchEffect(normalized, effectExecution));
1435
+ executionOptions.requestBudget.check();
1378
1436
  if (normalized.kind === 'config.get' || normalized.kind === 'secret.get') {
1379
1437
  if (snapshot !== undefined && typeof snapshot !== 'string') {
1380
1438
  throw new CanonicalRuntimeError(
@@ -1395,6 +1453,7 @@ function createCanonicalHostRuntime(options = {}) {
1395
1453
  { receivedType: Array.isArray(snapshot) ? 'array' : snapshot === null ? 'null' : typeof snapshot }
1396
1454
  );
1397
1455
  }
1456
+ if (normalized.kind === 'time.now') snapshot = require('@pulse-compute/runtime/host').normalizeTimeResult(snapshot);
1398
1457
  if (normalized.kind === 'event.emit' && snapshot !== undefined) {
1399
1458
  throw new CanonicalRuntimeError(
1400
1459
  'EventAcceptanceError',
@@ -1467,7 +1526,7 @@ function createCanonicalHostRuntime(options = {}) {
1467
1526
  const generator = programModule.createHandler()(context.ctx, pulse);
1468
1527
  if (!generator || typeof generator.next !== 'function') throw new CanonicalRuntimeError('CanonicalHandlerProtocolError', 'PULSE_CANONICAL_HANDLER_PROTOCOL', 'Compiled canonical handler did not return a generator.');
1469
1528
  let step;
1470
- try { step = generator.next(); }
1529
+ try { executionOptions.requestBudget.check(); step = generator.next(); }
1471
1530
  catch (error) { error.execution = { executionId, trace: [...trace], continuations: executionContinuations() }; throw error; }
1472
1531
 
1473
1532
  while (!step.done) {
@@ -1481,6 +1540,7 @@ function createCanonicalHostRuntime(options = {}) {
1481
1540
  let value;
1482
1541
  try {
1483
1542
  value = marker.kind === 'group' ? await dispatchGroup(effects) : await dispatchOne(effects[0]);
1543
+ executionOptions.requestBudget.check();
1484
1544
  registry.resume(continuationId);
1485
1545
  recordTrace(Object.freeze({ type: 'continuation-resumed', provider: adapter.id, executionId, continuationId, branchPoint: marker.continuationId }));
1486
1546
  step = generator.next(value);
@@ -1497,7 +1557,7 @@ function createCanonicalHostRuntime(options = {}) {
1497
1557
  }
1498
1558
 
1499
1559
  let response;
1500
- try { response = finalResponse(step.value, context.request.method); }
1560
+ try { executionOptions.requestBudget.check(); response = finalResponse(step.value, context.request.method); }
1501
1561
  catch (error) {
1502
1562
  error.execution = Object.freeze({ executionId, trace: Object.freeze([...trace]), continuations: Object.freeze(executionContinuations()), resolutionOrder: Object.freeze([...resolutionOrder]) });
1503
1563
  throw error;
@@ -1550,6 +1610,7 @@ module.exports = {
1550
1610
  CanonicalRuntimeError,
1551
1611
  assertCanonicalProgramCompatibility,
1552
1612
  createCanonicalHostRuntime,
1613
+ createRequestBudget: portableKv.createRequestBudget,
1553
1614
  executeCanonicalProgram,
1554
1615
  createCanonicalContext,
1555
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');
@@ -655,6 +666,8 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
655
666
  router_match(pathHandle, patternHandle) { return put(routerMatch(value(pathHandle), value(patternHandle))); },
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))); },
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))); },
658
671
  response_text(valueHandle, optionsHandle) { requireHttpSurface('ctx.text'); return put(context.ctx.text(value(valueHandle), value(optionsHandle))); },
659
672
  response_custom(valueHandle) { requireHttpSurface('ctx.response'); return put(context.ctx.response(value(valueHandle))); },
660
673
  grip_is_websocket() {
@@ -747,6 +760,24 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
747
760
  else env[item.name] = () => { throw new CanonicalNativeHostError(`Unsupported AssemblyScript env import ${item.name}.`, 'PULSE_CANONICAL_NATIVE_ENV_IMPORT_UNSUPPORTED', { name: item.name }); };
748
761
  }
749
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; };
750
781
  instance = new WebAssembly.Instance(module, { pulse_host: pulseHost, env });
751
782
  if (schemaIndexes.size > 0) {
752
783
  const available = new Set(moduleShape.exports.map((entry) => entry.name));
@@ -789,6 +820,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
789
820
  }
790
821
 
791
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);
792
824
  if (effect.kind === 'event.emit' && result !== undefined) {
793
825
  throw new canonicalRuntime.CanonicalRuntimeError(
794
826
  'EventAcceptanceError',
@@ -896,6 +928,7 @@ function instantiateCanonicalNativeModule(compiled, options = {}) {
896
928
  resume() { return instance.exports.pulse_resume(); },
897
929
  pendingEffects: takePending,
898
930
  prepareEffectResult,
931
+ captureApplicationError,
899
932
  setEffectResult,
900
933
  resultValue,
901
934
  response() { requireHttpSurface('HTTP completion'); return canonicalRuntime.finalResponse(resultValue(), context.ctx.req.method); },
@@ -914,9 +947,18 @@ function nativeEventCancelled(message) {
914
947
  return new CanonicalNativeHostError(message, 'PULSE_CANONICAL_NATIVE_EVENT_CANCELLED');
915
948
  }
916
949
 
917
- 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) {
918
958
  if (!signal) return Promise.resolve(value);
919
- 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());
920
962
  return new Promise((resolve, reject) => {
921
963
  let settled = false;
922
964
  const finish = (callback, result) => {
@@ -925,7 +967,7 @@ function raceNativeEventSignal(value, signal) {
925
967
  signal.removeEventListener('abort', onAbort);
926
968
  callback(result);
927
969
  };
928
- const onAbort = () => finish(reject, nativeEventCancelled('Native event execution was cancelled during effect dispatch.'));
970
+ const onAbort = () => finish(reject, cancelled());
929
971
  signal.addEventListener('abort', onAbort, { once: true });
930
972
  Promise.resolve(value).then(
931
973
  (result) => finish(resolve, result),
@@ -954,14 +996,17 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
954
996
  let executionFailure;
955
997
  let activeContinuation;
956
998
  try {
999
+ if (!eventMode && options.signal?.aborted) throw nativeRequestCancelled(options.signal);
957
1000
  if (eventMode && options.signal && options.signal.aborted) {
958
1001
  throw nativeEventCancelled('Native event execution was cancelled before handler entry.');
959
1002
  }
1003
+ options.requestBudget?.check();
960
1004
  status = eventMode ? controller.startEvent(invocation.frame) : controller.start();
961
1005
  while (status === runtimeContract.CANONICAL_NATIVE_RUN_STATUS.SUSPENDED) {
962
1006
  if (eventMode && options.signal && options.signal.aborted) {
963
1007
  throw nativeEventCancelled('Native event execution was cancelled while suspended.');
964
1008
  }
1009
+ options.requestBudget?.check();
965
1010
  const pending = controller.pendingEffects();
966
1011
  if (pending.length !== controller.exports.pulse_pending_count()) {
967
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 });
@@ -994,7 +1039,8 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
994
1039
  }
995
1040
  effectCount += pending.length;
996
1041
 
997
- const settled = await Promise.allSettled(pending.map(async (entry) => {
1042
+ const settled = await raceNativeSignal(Promise.allSettled(pending.map(async (entry) => {
1043
+ options.requestBudget?.check();
998
1044
  if (eventMode && options.signal && options.signal.aborted) {
999
1045
  throw nativeEventCancelled('Native event execution was cancelled before effect dispatch.');
1000
1046
  }
@@ -1032,19 +1078,27 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
1032
1078
  const rawResult = conditional
1033
1079
  ? await portableKv.executeConditionalKv(normalized, adapter.prepareConditionalKv || ((_admitted, kvExecution) => () => adapter.dispatchEffect(normalized, kvExecution)),
1034
1080
  { ...effectExecution, onKvObservation: (event) => controller.trace.push(event) }, options)
1035
- : await raceNativeEventSignal(adapter.dispatchEffect(normalized, effectExecution), eventMode ? options.signal : undefined);
1081
+ : await raceNativeSignal(adapter.dispatchEffect(normalized, effectExecution), options.signal, eventMode);
1082
+ options.requestBudget?.check();
1036
1083
  const result = controller.prepareEffectResult(entry.index, rawResult);
1037
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)));
1038
1085
  resolutionOrder.push(entry.effect.id);
1039
1086
  return { entry, result };
1040
- }));
1087
+ })), options.signal, eventMode);
1041
1088
 
1042
1089
  if (eventMode && options.signal && options.signal.aborted) {
1043
1090
  throw nativeEventCancelled('Native event execution was cancelled while suspended.');
1044
1091
  }
1045
- const failed = settled.find((entry) => entry.status === 'rejected');
1046
- if (failed) throw canonicalRuntime.redactRuntimeError(failed.reason, controller.sensitiveValues);
1047
- 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));
1048
1102
  const previousPc = controller.programCounter();
1049
1103
  activeContinuation.state = 'resumed';
1050
1104
  activeContinuation.states.push('resumed');
@@ -1052,8 +1106,8 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
1052
1106
  if (status === runtimeContract.CANONICAL_NATIVE_RUN_STATUS.INVALID_RESUME) {
1053
1107
  throw new CanonicalNativeHostError('Native module rejected a complete effect-result set.', 'PULSE_CANONICAL_NATIVE_INVALID_RESUME', { programCounter: previousPc, errorCode: controller.lastErrorCode() });
1054
1108
  }
1055
- activeContinuation.state = 'completed';
1056
- activeContinuation.states.push('completed');
1109
+ activeContinuation.state = failed ? 'failed' : 'completed';
1110
+ activeContinuation.states.push(activeContinuation.state);
1057
1111
  controller.trace.push(Object.freeze(redactValue({ type: 'native-continuation-resumed', executionId, continuationId: continuation.id, continuationState }, controller.sensitiveValues)));
1058
1112
  activeContinuation = undefined;
1059
1113
  }
@@ -1085,6 +1139,8 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
1085
1139
  valueHandleCount: controller.heap.size()
1086
1140
  });
1087
1141
  }
1142
+ if (options.signal?.aborted) throw nativeRequestCancelled(options.signal);
1143
+ options.requestBudget?.check();
1088
1144
  const response = controller.response();
1089
1145
  controller.trace.push(Object.freeze(redactValue({ type: 'native-execution-completed', executionId, provider: adapter.id, status: response.status, bodyClass: response.bodyClass }, controller.sensitiveValues)));
1090
1146
  return Object.freeze({
@@ -1125,7 +1181,11 @@ async function executeCanonicalNativeInvocation(compiled, options = {}, invocati
1125
1181
  }
1126
1182
 
1127
1183
  async function executeCanonicalNativeModule(compiled, options = {}) {
1128
- 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(); }
1129
1189
  }
1130
1190
 
1131
1191
  function nativeEventFailure(error) {