@junejs/server 1.0.0-dev.10 → 1.0.0-dev.12

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.
@@ -66,6 +66,7 @@ declare class AgentDurableObject {
66
66
  private readonly sink;
67
67
  private readonly model;
68
68
  private readonly tools;
69
+ private readonly channels;
69
70
  private readonly channelInstructions?;
70
71
  private readonly resources;
71
72
  private readonly services;
@@ -1,5 +1,6 @@
1
+ import { isolateLocal } from "./isolate-local.js";
1
2
  import { assertCoreRuntimeVersion } from "./core-version.js";
2
- import { channelDispatch, resolveChannel } from "@junejs/core/agent-config";
3
+ import { DeliverUnsupportedError, channelDispatch, resolveChannel } from "@junejs/core/agent-config";
3
4
  import { AgentSession, ResumeAuthorizationError, withSystem } from "@junejs/core/agent-runtime";
4
5
  import { ensureScope, runInScope } from "@junejs/db";
5
6
  //#region src/agent-durable.ts
@@ -80,6 +81,7 @@ var AgentDurableObject = class {
80
81
  sink;
81
82
  model;
82
83
  tools;
84
+ channels;
83
85
  channelInstructions;
84
86
  resources;
85
87
  services;
@@ -111,7 +113,8 @@ var AgentDurableObject = class {
111
113
  }
112
114
  defaultLog();
113
115
  });
114
- const channelTools = (def.channels ?? []).flatMap((c) => resolveChannel(c, def.env).tools?.() ?? []);
116
+ const channels = (def.channels ?? []).map((c) => resolveChannel(c, def.env));
117
+ const channelTools = channels.flatMap((c) => c.tools?.() ?? []);
115
118
  const tools = [...def.tools];
116
119
  const seen = new Set(tools.map((t) => t.spec.name));
117
120
  for (const t of channelTools) {
@@ -124,6 +127,7 @@ var AgentDurableObject = class {
124
127
  this.sink = sink;
125
128
  this.model = model;
126
129
  this.tools = tools;
130
+ this.channels = channels;
127
131
  this.channelInstructions = def.channelInstructions;
128
132
  }
129
133
  resolveSession(key) {
@@ -167,6 +171,13 @@ var AgentDurableObject = class {
167
171
  if (req.method === "POST" && url.pathname.endsWith("/turn")) {
168
172
  const { userText, turnId, event, trigger } = await req.json();
169
173
  await ensureScope();
174
+ const wantsDeliver = url.searchParams.get("deliver") === "1";
175
+ let deliverChannel;
176
+ if (wantsDeliver) {
177
+ if (!event?.channelId) return Response.json({ error: "deliver=1 needs an inbound event (event.channelId names the reply target)" }, { status: 400 });
178
+ deliverChannel = this.channels.find((c) => c.name === event.source);
179
+ if (!deliverChannel?.deliver) return Response.json({ error: `agent "${this.name}": no deliver()-capable channel named "${event.source}" is wired into this DO (DoAgentDef.channels) — the turn was NOT started` }, { status: 501 });
180
+ }
170
181
  let session;
171
182
  let started;
172
183
  try {
@@ -183,6 +194,25 @@ var AgentDurableObject = class {
183
194
  } catch (err) {
184
195
  return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
185
196
  }
197
+ if (wantsDeliver) {
198
+ const logRenderFailure = (err) => console.error(`[june] agent "${this.name}": delivered render for turn ${started.turnId} failed:`, err);
199
+ try {
200
+ const events = observeTurnEvents(session, started.turnId);
201
+ const target = {
202
+ channelId: event.channelId,
203
+ threadId: event.threadId,
204
+ recipientUserId: event.user?.id,
205
+ recipientTeamId: event.teamId
206
+ };
207
+ Promise.resolve().then(() => runInScope({
208
+ resources: this.resources,
209
+ services: this.services
210
+ }, () => deliverChannel.deliver(target, events, { session: this.sessionKey }))).catch(logRenderFailure);
211
+ } catch (err) {
212
+ logRenderFailure(err);
213
+ }
214
+ return Response.json({ turnId: started.turnId }, { status: 202 });
215
+ }
186
216
  if (url.searchParams.get("detach") === "1") return Response.json({ turnId: started.turnId }, { status: 202 });
187
217
  return new Response(sseTurnStream(session, started.turnId), { headers: SSE_HEADERS });
188
218
  }
@@ -247,6 +277,49 @@ function sseTurnStream(session, turnId) {
247
277
  }
248
278
  });
249
279
  }
280
+ function observeTurnEvents(session, turnId) {
281
+ const queue = [];
282
+ let terminal = false;
283
+ let notify;
284
+ const unsub = session.observe((e) => {
285
+ queue.push(e);
286
+ if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") terminal = true;
287
+ notify?.();
288
+ }, { turnId });
289
+ return { [Symbol.asyncIterator]() {
290
+ return {
291
+ async next() {
292
+ for (;;) {
293
+ const e = queue.shift();
294
+ if (e) return {
295
+ value: e,
296
+ done: false
297
+ };
298
+ if (terminal) {
299
+ unsub();
300
+ return {
301
+ value: void 0,
302
+ done: true
303
+ };
304
+ }
305
+ await new Promise((resolve) => {
306
+ notify = resolve;
307
+ });
308
+ notify = void 0;
309
+ }
310
+ },
311
+ async return() {
312
+ unsub();
313
+ terminal = true;
314
+ queue.length = 0;
315
+ return {
316
+ value: void 0,
317
+ done: true
318
+ };
319
+ }
320
+ };
321
+ } };
322
+ }
250
323
  async function sseTurnFinalText(res) {
251
324
  const ct = res.headers.get("content-type") ?? "";
252
325
  if (!res.body || !ct.includes("text/event-stream")) {
@@ -325,6 +398,18 @@ function durableAgentSurface(getNamespace, opts) {
325
398
  return Response.json({ text: await sseTurnFinalText(res) });
326
399
  };
327
400
  }
401
+ function resolveServices(factory, env, agentName) {
402
+ if (!factory) return void 0;
403
+ if (typeof env !== "object" || env === null) return factory(env);
404
+ const cache = isolateLocal("june.durableChannelSurface.services", () => /* @__PURE__ */ new WeakMap());
405
+ let perAgent = cache.get(env);
406
+ if (!perAgent) {
407
+ perAgent = /* @__PURE__ */ new Map();
408
+ cache.set(env, perAgent);
409
+ }
410
+ if (!perAgent.has(agentName)) perAgent.set(agentName, factory(env));
411
+ return perAgent.get(agentName);
412
+ }
328
413
  function durableChannelSurface(getNamespace, opts) {
329
414
  return channelDispatch(opts.channels.map((c) => resolveChannel(c, opts.env)), {
330
415
  agent: {
@@ -335,7 +420,7 @@ function durableChannelSurface(getNamespace, opts) {
335
420
  channels: [],
336
421
  connections: []
337
422
  },
338
- services: opts.services?.(opts.env),
423
+ services: resolveServices(opts.services, opts.env, opts.agentName),
339
424
  waitUntil: opts.waitUntil,
340
425
  run: async (message, o) => {
341
426
  const namespace = getNamespace();
@@ -358,6 +443,20 @@ function durableChannelSurface(getNamespace, opts) {
358
443
  }
359
444
  return await res.json();
360
445
  },
446
+ runDelivered: async (message, o) => {
447
+ const namespace = getNamespace();
448
+ if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
449
+ const res = await durableFetch(namespace, opts.agentName, o?.session ?? "default", new Request("https://do/turn?deliver=1", {
450
+ method: "POST",
451
+ body: serializeTurn(message, o)
452
+ }));
453
+ if (res.status === 501) throw new DeliverUnsupportedError(`runDelivered: ${(await res.json().catch(() => void 0))?.error ?? "the turn host cannot deliver this turn"}`);
454
+ if (res.status !== 202) {
455
+ const detail = (await res.text()).slice(0, 200);
456
+ throw new Error(`runDelivered: delivered turn was not accepted (status ${res.status}): ${detail}`);
457
+ }
458
+ return await res.json();
459
+ },
361
460
  runStream: async function* (message, o) {
362
461
  const namespace = getNamespace();
363
462
  if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
@@ -0,0 +1,6 @@
1
+ //#region src/isolate-local.d.ts
2
+ type IsolateLocal = <T>(key: string | symbol, make: () => T) => T;
3
+ declare function makeIsolateLocalFallback(): IsolateLocal;
4
+ declare const isolateLocal: IsolateLocal;
5
+ //#endregion
6
+ export { IsolateLocal, isolateLocal, makeIsolateLocalFallback };
@@ -0,0 +1,12 @@
1
+ import * as juneDb from "@junejs/db";
2
+ //#region src/isolate-local.ts
3
+ function makeIsolateLocalFallback() {
4
+ const values = /* @__PURE__ */ new Map();
5
+ return (key, make) => {
6
+ if (!values.has(key)) values.set(key, make());
7
+ return values.get(key);
8
+ };
9
+ }
10
+ const isolateLocal = juneDb.isolateLocal ?? makeIsolateLocalFallback();
11
+ //#endregion
12
+ export { isolateLocal, makeIsolateLocalFallback };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junejs/server",
3
- "version": "1.0.0-dev.10",
3
+ "version": "1.0.0-dev.12",
4
4
  "description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -125,14 +125,14 @@
125
125
  "build": "tsdown"
126
126
  },
127
127
  "dependencies": {
128
- "@junejs/db": ">=0.0.34-dev.1",
128
+ "@junejs/db": ">=0.1.0-dev.2",
129
129
  "@momiji-rs/sparkdown": "^0.0.4",
130
130
  "lightningcss": "^1.27.0",
131
131
  "oxc-parser": "^0.137.0",
132
132
  "rolldown": "^1.1.0"
133
133
  },
134
134
  "peerDependencies": {
135
- "@junejs/core": ">=0.2.0-dev.26",
135
+ "@junejs/core": ">=0.2.0-dev.27",
136
136
  "react": "^19.2.0",
137
137
  "react-dom": "^19.2.0",
138
138
  "react-server-dom-webpack": "19.2.7",
@@ -34,6 +34,7 @@ import {
34
34
  import type { Resources } from "@junejs/core/resources";
35
35
  import {
36
36
  channelDispatch,
37
+ DeliverUnsupportedError,
37
38
  resolveChannel,
38
39
  type AgentDefinition,
39
40
  type Channel,
@@ -41,6 +42,7 @@ import {
41
42
  type ChannelFactory,
42
43
  } from "@junejs/core/agent-config";
43
44
  import { ensureScope, runInScope } from "@junejs/db";
45
+ import { isolateLocal } from "./isolate-local";
44
46
  import { assertCoreRuntimeVersion } from "./core-version";
45
47
 
46
48
  // ── minimal structural Cloudflare surface (no @cloudflare/workers-types dep) ──
@@ -226,6 +228,7 @@ export class AgentDurableObject {
226
228
  private readonly sink: InProcEventSink;
227
229
  private readonly model: Model;
228
230
  private readonly tools: Tool[];
231
+ private readonly channels: Channel[];
229
232
  private readonly channelInstructions?: Record<string, string>;
230
233
  // Built from the DO's env in the constructor (this isolate), shared across turns —
231
234
  // env is stable per isolate, like bindWorkerResources memoizes per worker isolate.
@@ -276,8 +279,11 @@ export class AgentDurableObject {
276
279
  });
277
280
  // Merge the mounted channels' capability tools (built here from this DO's env, since a
278
281
  // tool's `run` closure can't cross the RPC). The cross-channel source gate on each tool
279
- // keeps a Slack tool inert during a Crisp turn, so merging all of them is safe.
280
- const channelTools = (def.channels ?? []).flatMap((c) => resolveChannel(c, def.env).tools?.() ?? []);
282
+ // keeps a Slack tool inert during a Crisp turn, so merging all of them is safe. The
283
+ // resolved channels are also RETAINED (this.channels): a delivered turn (/turn?deliver=1)
284
+ // renders its reply through the source channel's deliver() from inside this DO.
285
+ const channels = (def.channels ?? []).map((c) => resolveChannel(c, def.env));
286
+ const channelTools = channels.flatMap((c) => c.tools?.() ?? []);
281
287
  const tools: Tool[] = [...def.tools];
282
288
  const seen = new Set(tools.map((t) => t.spec.name));
283
289
  for (const t of channelTools) {
@@ -290,6 +296,7 @@ export class AgentDurableObject {
290
296
  this.sink = sink;
291
297
  this.model = model;
292
298
  this.tools = tools;
299
+ this.channels = channels;
293
300
  this.channelInstructions = def.channelInstructions;
294
301
  }
295
302
  // Resolve THE session for this DO: explicit key (from a routed request) → persisted
@@ -370,6 +377,26 @@ export class AgentDurableObject {
370
377
  if (req.method === "POST" && url.pathname.endsWith("/turn")) {
371
378
  const { userText, turnId, event, trigger } = (await req.json()) as { userText: string; turnId?: string; event?: InboundEvent; trigger?: ProactiveTrigger };
372
379
  await ensureScope();
380
+ // DELIVERED: like detach=1, the caller gets a 202 and goes away — but the reply is NOT
381
+ // dropped: this DO renders the turn's event stream through the source channel's own
382
+ // deliver() (the inbound renderer), under the DO's lifetime. Capability is checked
383
+ // BEFORE the turn starts — that ordering is DeliverUnsupportedError's contract at the
384
+ // surface: on a pre-start rejection the caller may fall back to consumer-side rendering
385
+ // without double-running the turn.
386
+ const wantsDeliver = url.searchParams.get("deliver") === "1";
387
+ let deliverChannel: Channel | undefined;
388
+ if (wantsDeliver) {
389
+ if (!event?.channelId) {
390
+ return Response.json({ error: "deliver=1 needs an inbound event (event.channelId names the reply target)" }, { status: 400 });
391
+ }
392
+ deliverChannel = this.channels.find((c) => c.name === event.source);
393
+ if (!deliverChannel?.deliver) {
394
+ return Response.json(
395
+ { error: `agent "${this.name}": no deliver()-capable channel named "${event.source}" is wired into this DO (DoAgentDef.channels) — the turn was NOT started` },
396
+ { status: 501 },
397
+ );
398
+ }
399
+ }
373
400
  // start() schedules the turn on the chain WITHIN the scope, so it runs with ambient
374
401
  // db/services (ALS propagates to the .then continuation registered here); subscribing
375
402
  // happens synchronously right after, before any event can emit.
@@ -383,6 +410,35 @@ export class AgentDurableObject {
383
410
  // object's identity — a client-resolvable conflict, not a crash
384
411
  return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 409 });
385
412
  }
413
+ if (wantsDeliver) {
414
+ // Subscribe synchronously (same guarantee sseTurnStream relies on: start() scheduled
415
+ // the turn, no event can have emitted yet), then render under THIS DO's lifetime —
416
+ // the retained promise is pending work that keeps the DO alive, so a long turn's
417
+ // rendering survives however briefly the caller held its connection. The event is
418
+ // present (checked above); its author/thread are the reply target, exactly the
419
+ // surface the channel's inbound renderStream derives.
420
+ //
421
+ // Once start() has queued the turn, NOTHING here may escape: a throw would 500 the
422
+ // caller for a turn that runs anyway — the exact ambiguity delivered turns exist to
423
+ // remove. deliver() is app/third-party code that can throw synchronously before
424
+ // returning its promise, so it is deferred into the chain (the eager subscription
425
+ // above stays synchronous — that timing guarantee must not move); the try/catch
426
+ // backstops the subscription itself. Rendering failures have no live consumer —
427
+ // this log is their only surfacing path (the TURN's own failures already surface
428
+ // via the #76 sink subscription).
429
+ const logRenderFailure = (err: unknown) =>
430
+ console.error(`[june] agent "${this.name}": delivered render for turn ${started.turnId} failed:`, err);
431
+ try {
432
+ const events = observeTurnEvents(session, started.turnId);
433
+ const target = { channelId: event!.channelId, threadId: event!.threadId, recipientUserId: event!.user?.id, recipientTeamId: event!.teamId };
434
+ Promise.resolve()
435
+ .then(() => runInScope({ resources: this.resources, services: this.services }, () => deliverChannel!.deliver!(target, events, { session: this.sessionKey })))
436
+ .catch(logRenderFailure);
437
+ } catch (err) {
438
+ logRenderFailure(err);
439
+ }
440
+ return Response.json({ turnId: started.turnId }, { status: 202 });
441
+ }
386
442
  // DETACHED (#77): the turn is accepted — 202 now, no stream. It keeps running under
387
443
  // this DO's lifetime (alive while work is pending), so its duration is no longer
388
444
  // bounded by however long the CALLER can hold a connection (the edge waitUntil
@@ -457,6 +513,45 @@ function sseTurnStream(session: AgentSession, turnId: string): ReadableStream<Ui
457
513
  });
458
514
  }
459
515
 
516
+ // The IN-PROCESS sibling of sseTurnStream: one turn's TurnEvents as an AsyncIterable, for a
517
+ // consumer living in the SAME isolate as the session (a delivered render). Subscribes eagerly
518
+ // at call time — not at first iteration — so the sseTurnStream timing guarantee carries over
519
+ // (call this synchronously after start(), before any event can emit); events landing before
520
+ // the consumer catches up are buffered. Ends after the turn's terminal event (completed,
521
+ // failed, or input.requested — a park ends this stream; a later /resume is a new one), and an
522
+ // early consumer exit (for-await break/return) unsubscribes rather than buffering forever.
523
+ function observeTurnEvents(session: AgentSession, turnId: string): AsyncIterable<TurnEvent> {
524
+ const queue: TurnEvent[] = [];
525
+ let terminal = false;
526
+ let notify: (() => void) | undefined;
527
+ const unsub = session.observe((e) => {
528
+ queue.push(e);
529
+ if (e.type === "turn.completed" || e.type === "turn.failed" || e.type === "input.requested") terminal = true;
530
+ notify?.();
531
+ }, { turnId });
532
+ return {
533
+ [Symbol.asyncIterator]() {
534
+ return {
535
+ async next(): Promise<IteratorResult<TurnEvent>> {
536
+ for (;;) {
537
+ const e = queue.shift();
538
+ if (e) return { value: e, done: false };
539
+ if (terminal) { unsub(); return { value: undefined, done: true }; }
540
+ await new Promise<void>((resolve) => { notify = resolve; });
541
+ notify = undefined;
542
+ }
543
+ },
544
+ async return(): Promise<IteratorResult<TurnEvent>> {
545
+ unsub();
546
+ terminal = true;
547
+ queue.length = 0;
548
+ return { value: undefined, done: true };
549
+ },
550
+ };
551
+ },
552
+ };
553
+ }
554
+
460
555
  // Consume an SSE turn stream to its terminal state: the final text, or throw on failure.
461
556
  // The non-streaming path (channels, a JSON chat response) uses this. Guards a non-SSE /
462
557
  // body-less upstream response (a misroute or an error) into a clear error, not a TypeError.
@@ -605,6 +700,43 @@ export function durableAgentSurface(
605
700
  // fails in the background — surfaced via the channel's onError IF one is configured,
606
701
  // otherwise swallowed (runBackground never rejects). The app is expected to bind
607
702
  // env.AGENT when it mounts channels. Returns null for unclaimed requests.
703
+ // The services bag, memoized per (env object, agentName).
704
+ //
705
+ // Why that key. `env` identity is what makes the memo survive: a worker builds
706
+ // the surface inside `fetch`, so the only thing stable across requests is the
707
+ // env object the host hands in — and NOT the provider function, which the
708
+ // documented shape (`services: (e) => makeServices(e)`) recreates on every
709
+ // fetch. But env alone is too coarse: two surfaces mounted in one worker over
710
+ // the same env would then share whichever bag was built first, making the
711
+ // result depend on mount order. `agentName` is the surface's own stable
712
+ // identity, so it partitions them. (One agent must therefore have ONE services
713
+ // provider — mounting the same agentName twice with different providers is
714
+ // incoherent and keeps the first bag.)
715
+ //
716
+ // Held in an isolateLocal so duplicate module instances (workspace symlinks)
717
+ // share one cache; the outer WeakMap is keyed by env so a host handing out
718
+ // fresh env objects gets fresh bags instead of a leak. A non-object env (tests
719
+ // passing a primitive) skips memoization rather than throwing.
720
+ function resolveServices(
721
+ factory: ((env: unknown) => unknown) | undefined,
722
+ env: unknown,
723
+ agentName: string,
724
+ ): unknown {
725
+ if (!factory) return undefined;
726
+ if (typeof env !== "object" || env === null) return factory(env);
727
+ const cache = isolateLocal(
728
+ "june.durableChannelSurface.services",
729
+ () => new WeakMap<object, Map<string, unknown>>(),
730
+ );
731
+ let perAgent = cache.get(env);
732
+ if (!perAgent) {
733
+ perAgent = new Map<string, unknown>();
734
+ cache.set(env, perAgent);
735
+ }
736
+ if (!perAgent.has(agentName)) perAgent.set(agentName, factory(env));
737
+ return perAgent.get(agentName);
738
+ }
739
+
608
740
  export function durableChannelSurface(
609
741
  getNamespace: () => DurableObjectNamespace | undefined,
610
742
  opts: {
@@ -614,7 +746,17 @@ export function durableChannelSurface(
614
746
  // The app's services factory — the SAME one AgentDurableObject uses — resolved HERE
615
747
  // (worker isolate) and exposed to channel hooks as ctx.services. Channel hooks run at
616
748
  // the edge, outside the DO, so they can't read the DO's ambient currentServices(); this
617
- // gives them the same DI bag. Resolved once per surface construction.
749
+ // gives them the same DI bag.
750
+ //
751
+ // Called once per (env object, agentName) — NOT once per surface. A worker
752
+ // typically constructs the surface inside `fetch` (env only exists inside an
753
+ // invocation), so resolving per construction rebuilt every client on every
754
+ // request and any cache the bag held could never hit. Memoizing on env
755
+ // identity fixes that; see resolveServices above for why the key is env ×
756
+ // agentName and not the provider function. Practical consequences: the
757
+ // provider must be a function of `env` alone, it runs again whenever the
758
+ // host hands in a different env object (a new isolate, or a test), and one
759
+ // agentName must have one provider.
618
760
  services?: (env: unknown) => unknown;
619
761
  waitUntil?: (p: Promise<unknown>) => void;
620
762
  },
@@ -624,7 +766,7 @@ export function durableChannelSurface(
624
766
  // A minimal but complete AgentDefinition — channels only read ctx.agent.name; the
625
767
  // full def isn't present in the worker (tools/model live in the DO).
626
768
  agent: { name: opts.agentName, instructions: "", tools: [], skills: [], channels: [], connections: [] } satisfies AgentDefinition,
627
- services: opts.services?.(opts.env),
769
+ services: resolveServices(opts.services, opts.env, opts.agentName),
628
770
  waitUntil: opts.waitUntil,
629
771
  run: async (message, o) => {
630
772
  const namespace = getNamespace();
@@ -657,6 +799,30 @@ export function durableChannelSurface(
657
799
  }
658
800
  return (await res.json()) as { turnId: string };
659
801
  },
802
+ // DELIVERED: runDetached's reply-bearing sibling — the DO runs the turn AND renders its
803
+ // reply through the source channel's deliver() under its OWN lifetime, so a reply-bearing
804
+ // turn escapes the edge waitUntil ceiling exactly as a shadow turn does. A 501 means the
805
+ // DO refused BEFORE starting the turn (the channel isn't wired there / has no deliver) —
806
+ // surfaced as DeliverUnsupportedError, the one rejection a channel may answer with a
807
+ // consumer-side rendering fallback without double-running the turn.
808
+ runDelivered: async (message, o) => {
809
+ const namespace = getNamespace();
810
+ if (!namespace) throw new Error("durableChannelSurface: no Durable Object namespace bound (env.AGENT)");
811
+ const res = await durableFetch(
812
+ namespace,
813
+ opts.agentName,
814
+ o?.session ?? "default",
815
+ new Request("https://do/turn?deliver=1", { method: "POST", body: serializeTurn(message, o) }),
816
+ );
817
+ if (res.status === 501) {
818
+ throw new DeliverUnsupportedError(`runDelivered: ${((await res.json().catch(() => undefined)) as { error?: string } | undefined)?.error ?? "the turn host cannot deliver this turn"}`);
819
+ }
820
+ if (res.status !== 202) {
821
+ const detail = (await res.text()).slice(0, 200);
822
+ throw new Error(`runDelivered: delivered turn was not accepted (status ${res.status}): ${detail}`);
823
+ }
824
+ return (await res.json()) as { turnId: string };
825
+ },
660
826
  // The LIVE path: hand the channel the TurnEvent stream so it can render as the turn runs.
661
827
  runStream: async function* (message, o) {
662
828
  const namespace = getNamespace();
@@ -0,0 +1,33 @@
1
+ // isolate-local.ts — read @junejs/db's `isolateLocal` WITHOUT a named import.
2
+ //
3
+ // Same hazard core-version.ts exists for: a NAMED import of an export the
4
+ // resolved copy doesn't have dies at module LINK time with an unhelpful
5
+ // SyntaxError, before any of our own diagnostics can run. `isolateLocal` is new
6
+ // in @junejs/db 0.1.0-dev.2, and a version range cannot save us here — the
7
+ // floor has to stay satisfiable by the workspace's own db (which is the very
8
+ // version that lacks it), and `bun pm pack` rewrites a `workspace:*` dep to
9
+ // whatever version it resolves, which can be OLDER still.
10
+ //
11
+ // So: namespace import + optional read, with a local fallback. Unlike the core
12
+ // runtime contract, this one does NOT warrant throwing — isolate-scoped
13
+ // memoization is a lifetime optimization, not a correctness contract. The
14
+ // fallback memoizes in this module instance, so callers keep their cache; the
15
+ // only thing lost against an older db is sharing one value across DUPLICATE
16
+ // copies of the module (workspace symlinks), which costs a missed cache hit,
17
+ // never a wrong answer.
18
+ import * as juneDb from "@junejs/db";
19
+
20
+ export type IsolateLocal = <T>(key: string | symbol, make: () => T) => T;
21
+
22
+ // Exported for tests: the ambient read below can only ever see the workspace's
23
+ // own (matching) db, so the degraded path needs a direct handle to be provable.
24
+ export function makeIsolateLocalFallback(): IsolateLocal {
25
+ const values = new Map<string | symbol, unknown>();
26
+ return <T>(key: string | symbol, make: () => T): T => {
27
+ if (!values.has(key)) values.set(key, make());
28
+ return values.get(key) as T;
29
+ };
30
+ }
31
+
32
+ export const isolateLocal: IsolateLocal =
33
+ (juneDb as { isolateLocal?: IsolateLocal }).isolateLocal ?? makeIsolateLocalFallback();