@mcp-b/do-runtime 0.1.2 → 0.2.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 31cb2a6: Gate the streams `pipeThrough` and `pipeTo` produce. Native pipe machinery reads a gated body through internal spec operations and hands back a brand-new uninstrumented stream, so `res.body.pipeThrough(new TextDecoderStream()).getReader().read()` — the MCP SDK's SSE path — resumed foreign on every chunk and the next storage call threw "no input lock available in this context". `pipeThrough` now re-gates the readable it returns (recursively, so chains stay covered) and `pipeTo`'s settlement resumes gated.
8
+
9
+ ## 0.2.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 5c91da1: Name the last gated site in the "no input lock available in this context" error. The throw lands at the next storage call, which can be several layers past the foreign await that actually dropped the lock; the error now carries where the gate was last engaged — an `awaitIo` call site with its stack, an `entry` dispatch with its method name, a re-entry callback's registration site — and how many milliseconds before the throw, which brackets the offending await between two coordinates.
14
+
15
+ ## 0.2.0 — 2026-08-21
16
+
17
+ ### Added
18
+
19
+ - Expose `createDurableObjectNamespace()` and its placement-channel types for standard named Durable Object bindings.
20
+ - Run Agents SDK `routeAgentRequest()`, `getAgentByName()` direct stubs, decorated callables, streaming RPC, email routing, and sub-agents through the MV3 extension host.
21
+
22
+ ### Fixed
23
+
24
+ - Preserve named facet identities instead of re-hashing serialized Durable Object IDs.
25
+ - Re-enter the calling actor's input gate after outbound namespace-stub calls.
26
+ - Preserve browser WebSocket upgrade requests across PartyServer request clones.
27
+
3
28
  ## 0.1.2
4
29
 
5
30
  ### Patch Changes
package/README.md CHANGED
@@ -186,6 +186,15 @@ The lifecycle:
186
186
  5. Reach the platform through `container.globals` (or install it with `installActorScope`). For a host-provided promise an actor must await, wrap it once in `container.awaitIo()`.
187
187
  6. Watch `container.onBroken`; dispose the placement; recreate it on the next event over the same storage.
188
188
 
189
+ For a standard Durable Object binding, call
190
+ `createDurableObjectNamespace(uniqueKey, channel)` and put the result in `env`
191
+ and `ctx.exports`. The channel maps each routed id to a placed `Fetcher`; that
192
+ binding works directly with Agents SDK `routeAgentRequest()` and
193
+ `getAgentByName()`. When an actor uses the binding to call another actor, wrap
194
+ the transport promise with the caller's `container.awaitIo()` so its continuation
195
+ re-enters the owning input gate. The extension example shows both the external
196
+ router binding and the per-container actor binding.
197
+
189
198
  ### Storage
190
199
 
191
200
  `SqlDatabaseProvider.open(name)` is the runtime execution seam. The runtime owns database names, tables, transactions, reset behaviour, facet metadata, and streaming `sql.ingest()` statement boundaries; the host chooses the physical provider and prefix. Stored KV values use structured-clone semantics across workerd, Node, and the browser; existing JSON rows remain readable. `_cf_` names are reserved to the runtime.
package/dist/index.js CHANGED
@@ -664,7 +664,28 @@ var INPUT_GATE_BROKEN_PREFIX = "broken.inputGateBroken; ";
664
664
  */
665
665
  function requireInputLock(ctx, op) {
666
666
  if (ctx.hasCurrent()) return;
667
- throw new Error(`${op}: no input lock available in this context`);
667
+ throw new Error(`${op}: no input lock available in this context${ctx.describeLostLock()}`);
668
+ }
669
+ /**
670
+ * A stack for `noteGateUse`, captured where user frames are still on the stack.
671
+ *
672
+ * The invocation stack's own push and pop are scheduler moments — `#runImpl`
673
+ * runs from a gate resumption and `#exit` from a `MessageChannel` callback — so
674
+ * a trace taken there names only runtime internals. The moments that still see
675
+ * the caller are the synchronous entries into the gate machinery: an `awaitIo`
676
+ * call, an `entry` dispatch, a callback's registration. V8's zero-cost async
677
+ * traces extend those with the awaiting chain, which is usually the frame the
678
+ * reader actually wants.
679
+ *
680
+ * The first slice drops the `Error` header (absent on SpiderMonkey) and the two
681
+ * runtime frames: this helper and the gate entry point that called it.
682
+ */
683
+ function captureGateStack() {
684
+ const stack = (/* @__PURE__ */ new Error()).stack;
685
+ if (stack === void 0) return void 0;
686
+ const frames = stack.split("\n");
687
+ const trimmed = frames.slice(frames[0]?.startsWith("Error") ? 3 : 2).join("\n");
688
+ return trimmed === "" ? void 0 : trimmed;
668
689
  }
669
690
  /**
670
691
  * The end of the microtask checkpoint — the moment `runImpl`'s `KJ_DEFER` fires.
@@ -1014,6 +1035,13 @@ var IoContext = class {
1014
1035
  * the overlapping slices happen to leave in.
1015
1036
  */
1016
1037
  #currentInputLocks = [];
1038
+ /**
1039
+ * Where this context's gate was last deliberately engaged, for
1040
+ * `describeLostLock`. One slot, overwritten on every engagement — the gate
1041
+ * serialises slices, so the latest note is the best available ancestor of
1042
+ * whatever continuation is running lockless now.
1043
+ */
1044
+ #lastGateUse;
1017
1045
  #abortException;
1018
1046
  #abortPromise;
1019
1047
  #rejectAbort;
@@ -1074,6 +1102,42 @@ var IoContext = class {
1074
1102
  return currentSlice === this;
1075
1103
  }
1076
1104
  /**
1105
+ * Record that user code just engaged this context's gate — an `awaitIo`, an
1106
+ * `entry` dispatch, a re-entry callback firing. No upstream analogue, because
1107
+ * upstream cannot lose the lock; here a continuation that awaits a promise
1108
+ * the runtime does not own comes back lockless, the throw lands at the next
1109
+ * storage call three layers later, and the gap between "where the code last
1110
+ * verifiably ran gated" and the throw site is exactly where the foreign await
1111
+ * hides. This is that first coordinate. Always on: the capture rides calls
1112
+ * that already allocate promise machinery, and a stack costs microseconds
1113
+ * against the diagnosis it replaces.
1114
+ */
1115
+ noteGateUse(what, stack) {
1116
+ this.#lastGateUse = {
1117
+ what,
1118
+ stack,
1119
+ at: this.now()
1120
+ };
1121
+ }
1122
+ /**
1123
+ * The suffix `requireInputLock` appends when the invocation stack is empty:
1124
+ * where this context's gate was last engaged, and how long before the throw.
1125
+ *
1126
+ * "Last engaged" is the honest claim, not "this continuation's ancestor" —
1127
+ * once the offending chain went lockless the gate reopened, so another slice
1128
+ * may have run in between and be the note this reports. In practice the loss
1129
+ * is discovered within the same event storm and the note is the parent; when
1130
+ * it is not, an engagement of this actor moments earlier is still the right
1131
+ * neighbourhood to search.
1132
+ */
1133
+ describeLostLock() {
1134
+ const use = this.#lastGateUse;
1135
+ if (use === void 0) return " (this context has never held its gate: the call arrived from outside any actor invocation)";
1136
+ const age = Math.round(this.now() - use.at);
1137
+ const stackSuffix = use.stack === void 0 ? "" : `, at:\n${use.stack}`;
1138
+ return ` (an await after the last gated point resumed from a promise the runtime does not own; the gate was last engaged by ${use.what} ${age}ms before this call${stackSuffix})`;
1139
+ }
1140
+ /**
1077
1141
  * ← `IoContext::getActorOrThrow()`. Upstream's throws when the request is not
1078
1142
  * an actor request; there is no such request here, so it is a plain accessor.
1079
1143
  */
@@ -1234,13 +1298,16 @@ var IoContext = class {
1234
1298
  makeReentryCallback(func) {
1235
1299
  this.#requireCurrent();
1236
1300
  const criticalSection = this.getCriticalSection();
1301
+ const registrationStack = captureGateStack();
1237
1302
  return async (...args) => {
1303
+ this.noteGateUse("a re-entry callback registered at the site below", registrationStack);
1238
1304
  const call = this.run((lock) => func(lock, ...args), criticalSection);
1239
1305
  this.addTask(call.then(() => {}, () => {}));
1240
1306
  return await call;
1241
1307
  };
1242
1308
  }
1243
1309
  awaitIo(promise, func = identity) {
1310
+ this.noteGateUse("awaitIo", captureGateStack());
1244
1311
  return this.#awaitIoImpl(promise, this.getCriticalSection(), func);
1245
1312
  }
1246
1313
  awaitIoWithInputLock(promise, func = identity) {
@@ -1250,6 +1317,7 @@ var IoContext = class {
1250
1317
  } catch (exception) {
1251
1318
  return Promise.reject(exception);
1252
1319
  }
1320
+ this.noteGateUse("awaitIoWithInputLock", captureGateStack());
1253
1321
  return this.#awaitIoImpl(promise, inputLock, func);
1254
1322
  }
1255
1323
  /**
@@ -1263,6 +1331,7 @@ var IoContext = class {
1263
1331
  */
1264
1332
  blockConcurrencyWhile(callback) {
1265
1333
  const lock = this.getInputLock();
1334
+ this.noteGateUse("blockConcurrencyWhile", captureGateStack());
1266
1335
  const criticalSection = lock.startCriticalSection();
1267
1336
  const { promise: result, resolve } = Promise.withResolvers();
1268
1337
  this.addTask((async () => {
@@ -1312,7 +1381,7 @@ var IoContext = class {
1312
1381
  */
1313
1382
  #requireCurrent() {
1314
1383
  const lock = this.#currentInputLocks.at(-1);
1315
- if (lock === void 0) throw new Error("no input lock available in this context");
1384
+ if (lock === void 0) throw new Error(`no input lock available in this context${this.describeLostLock()}`);
1316
1385
  return lock;
1317
1386
  }
1318
1387
  /** ← the far side of `runInContextScope`'s `KJ_DEFER`. */
@@ -3419,7 +3488,7 @@ var DurableObjectFacets = class {
3419
3488
  const id = options.id;
3420
3489
  return {
3421
3490
  actorClass: requireFacetClass(options.class).getChannel(),
3422
- id: id === void 0 ? this.#parentId : typeof id === "string" ? id : id.toString()
3491
+ id: id === void 0 ? this.#parentId : typeof id === "string" ? id : id.name ?? id.toString()
3423
3492
  };
3424
3493
  });
3425
3494
  return facetManager.getFacet(name, getStartInfo);
@@ -3681,6 +3750,8 @@ var BYOB_READER_UNGATABLE_MESSAGE = "getReader({ mode: 'byob' }): a BYOB reader
3681
3750
  function gateReadableStream(ctx, stream) {
3682
3751
  const getReader = stream.getReader.bind(stream);
3683
3752
  const tee = stream.tee.bind(stream);
3753
+ const pipeThrough = stream.pipeThrough.bind(stream);
3754
+ const pipeTo = stream.pipeTo.bind(stream);
3684
3755
  Object.defineProperties(stream, {
3685
3756
  getReader: {
3686
3757
  configurable: true,
@@ -3697,6 +3768,20 @@ function gateReadableStream(ctx, stream) {
3697
3768
  const [a, b] = tee();
3698
3769
  return [gateReadableStream(ctx, a), gateReadableStream(ctx, b)];
3699
3770
  }
3771
+ },
3772
+ pipeThrough: {
3773
+ configurable: true,
3774
+ writable: true,
3775
+ value(transform, options) {
3776
+ return gateReadableStream(ctx, pipeThrough(transform, options));
3777
+ }
3778
+ },
3779
+ pipeTo: {
3780
+ configurable: true,
3781
+ writable: true,
3782
+ value(destination, options) {
3783
+ return ctx.awaitIo(pipeTo(destination, options));
3784
+ }
3700
3785
  }
3701
3786
  });
3702
3787
  return stream;
@@ -6944,6 +7029,7 @@ var ActorContainerImpl = class {
6944
7029
  const cached = bound.get(property);
6945
7030
  if (cached !== void 0) return cached;
6946
7031
  const gated = async (...args) => {
7032
+ this.#ctx.noteGateUse(`entry ${String(property)}()`, captureGateStack());
6947
7033
  const result = await this.#ctx.run(() => this.#withExternalEntry(() => value.apply(subject, args)));
6948
7034
  await this.#ctx.waitForOutputLocks();
6949
7035
  return result;
@@ -7132,6 +7218,12 @@ async function createActorContainer(options) {
7132
7218
  return new ActorContainerImpl(options, db, tree, tree);
7133
7219
  }
7134
7220
  //#endregion
7221
+ //#region src/server/actor-namespace.ts
7222
+ /** Assemble the configured namespace binding a host places in `env`. */
7223
+ function createDurableObjectNamespace(uniqueKey, channel) {
7224
+ return new DurableObjectNamespace(channel, new ActorIdFactoryImpl(uniqueKey));
7225
+ }
7226
+ //#endregion
7135
7227
  //#region src/transport/rpc-session.ts
7136
7228
  /**
7137
7229
  * ← workerd `NO upstream correspondence (capnweb adaptation)`
@@ -7175,6 +7267,6 @@ function newRpcSession(port, localMain) {
7175
7267
  return newMessagePortRpcSession(port, localMain);
7176
7268
  }
7177
7269
  //#endregion
7178
- export { ACTOR_CLASS_SERIALIZATION_UNIMPLEMENTED_MESSAGE, ALARM_RETRY_MAX_TRIES, ALARM_RETRY_START_SECONDS, ALLOW_EXPERIMENTAL_MESSAGE, ALREADY_ACCEPTED_MESSAGE, AlarmInvocationInfo, AlarmScheduler, BYOB_READER_UNGATABLE_MESSAGE, DEAD_LOAD_CONTEXT_MESSAGE, DEFAULT_ALARM_OUTLET, FACET_ALARM_UNIMPLEMENTED_MESSAGE, FACET_NAME_MAX_LENGTH, FACET_TREE_MAX_DEPTH, FOREIGN_SLICE_MESSAGE, HIBERNATION_UNIMPLEMENTED_MESSAGE, LoopbackDurableObjectClass, NOT_BYTES_MESSAGE, NO_GLOBAL_OUTBOUND_MESSAGE, NO_MODULES_MESSAGE, PITR_UNIMPLEMENTED_MESSAGE, REPLICATION_UNIMPLEMENTED_MESSAGE, RETRY_BACKOFF_MAX, RETRY_JITTER_FACTOR, STREAMING_TAILS_EXPERIMENTAL_MESSAGE, WorkerLoader, WorkerStub, actorScopeBindings, alarmRetryDelayMs, asLoopbackDurableObjectClass, createActorContainer, gateRequestBody, installActorScope, jsModuleInPythonWorkerMessage, moduleFieldCountMessage, moduleNameMessage, newRpcSession, noFacets, notSerializableMessage, pythonModuleInJsWorkerMessage, typeScriptModuleNameMessage };
7270
+ export { ACTOR_CLASS_SERIALIZATION_UNIMPLEMENTED_MESSAGE, ALARM_RETRY_MAX_TRIES, ALARM_RETRY_START_SECONDS, ALLOW_EXPERIMENTAL_MESSAGE, ALREADY_ACCEPTED_MESSAGE, AlarmInvocationInfo, AlarmScheduler, BYOB_READER_UNGATABLE_MESSAGE, DEAD_LOAD_CONTEXT_MESSAGE, DEFAULT_ALARM_OUTLET, FACET_ALARM_UNIMPLEMENTED_MESSAGE, FACET_NAME_MAX_LENGTH, FACET_TREE_MAX_DEPTH, FOREIGN_SLICE_MESSAGE, HIBERNATION_UNIMPLEMENTED_MESSAGE, LoopbackDurableObjectClass, NOT_BYTES_MESSAGE, NO_GLOBAL_OUTBOUND_MESSAGE, NO_MODULES_MESSAGE, PITR_UNIMPLEMENTED_MESSAGE, REPLICATION_UNIMPLEMENTED_MESSAGE, RETRY_BACKOFF_MAX, RETRY_JITTER_FACTOR, STREAMING_TAILS_EXPERIMENTAL_MESSAGE, WorkerLoader, WorkerStub, actorScopeBindings, alarmRetryDelayMs, asLoopbackDurableObjectClass, createActorContainer, createDurableObjectNamespace, gateRequestBody, installActorScope, jsModuleInPythonWorkerMessage, moduleFieldCountMessage, moduleNameMessage, newRpcSession, noFacets, notSerializableMessage, pythonModuleInJsWorkerMessage, typeScriptModuleNameMessage };
7179
7271
 
7180
7272
  //# sourceMappingURL=index.js.map