@snowyroad/braid 0.94.0 → 0.95.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -400,7 +400,7 @@ var InviteExpiredError = class extends Error {
400
400
  }
401
401
  };
402
402
  async function redeemInvite(relayHttpUrl, code) {
403
- const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-JA7L7WBG.js");
403
+ const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-O3LXKBAJ.js");
404
404
  const r = await boundedJsonFetch2(`${relayHttpUrl.replace(/\/$/, "")}/invites/redeem`, {
405
405
  method: "POST",
406
406
  headers: { "content-type": "application/json" },
@@ -492,7 +492,7 @@ function validateReplacementToken(newToken, currentToken, nowMs = Date.now()) {
492
492
  return { ok: true };
493
493
  }
494
494
  async function mintAccessToken(relayHttpUrl, agentKey, fetchFn = fetch, dpopProof) {
495
- const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-JA7L7WBG.js");
495
+ const { boundedJsonFetch: boundedJsonFetch2 } = await import("./http-O3LXKBAJ.js");
496
496
  const r = await boundedJsonFetch2(`${relayHttpUrl.replace(/\/$/, "")}/agents/token`, {
497
497
  method: "POST",
498
498
  headers: {
@@ -4151,7 +4151,18 @@ var RelayClient = class {
4151
4151
  candidates: Array.isArray(msg.candidates) ? msg.candidates.map((c) => untrusted(String(c ?? ""))) : void 0,
4152
4152
  history,
4153
4153
  // BL-126: server-minted attribution key — pure pass-through, never invented by bridge.
4154
- dispatchId: typeof msg.dispatchId === "string" && msg.dispatchId.length > 0 ? msg.dispatchId : void 0
4154
+ dispatchId: typeof msg.dispatchId === "string" && msg.dispatchId.length > 0 ? msg.dispatchId : void 0,
4155
+ // D14: additive Lisa Loop signal — pass-through, mostly untransformed. Absent
4156
+ // on every non-lisa msg (msg.lisa undefined), so non-lisa signals are unaffected.
4157
+ // Defensive shape-narrowing for parity with the sibling fields above: a
4158
+ // malformed lisa (string/null/array) is dropped rather than passed through raw.
4159
+ // Review Minor 1 (T11 final wave): freshSession is ROUTING-relevant (D13 — it
4160
+ // decides whether the turn runs on the ephemeral one-shot session at all), so
4161
+ // unlike the other (display-only) lisa fields it is coerced to a STRICT boolean
4162
+ // here rather than passed through raw. A relay drift like `freshSession:"false"`
4163
+ // (string) is truthy in JS and would otherwise wrongly route to the ephemeral
4164
+ // path; `=== true` makes anything but the literal boolean true resolve false.
4165
+ lisa: typeof msg.lisa === "object" && msg.lisa !== null && !Array.isArray(msg.lisa) ? { ...msg.lisa, freshSession: msg.lisa.freshSession === true } : void 0
4155
4166
  };
4156
4167
  this.flowCbs.forEach((cb) => cb(signal));
4157
4168
  }
@@ -4818,7 +4829,7 @@ var RelayClient = class {
4818
4829
  /** Post a bounded-flow reply (turn or synthesis) to the flow-scoped endpoint.
4819
4830
  * agentId MUST be the agent NAME — the relay's flow gate resolves turn ownership and
4820
4831
  * synthesis role via resolveAgentUUID (a name lookup); a UUID resolves to uuid.Nil -> 403. */
4821
- async postFlowMessage(channelId, flowId, content, usage, dispatchId) {
4832
+ async postFlowMessage(channelId, flowId, content, usage, dispatchId, lisaFresh) {
4822
4833
  const ch = this.pathId(channelId, "channelId");
4823
4834
  const fl = this.pathId(flowId, "flowId");
4824
4835
  if (!ch || !fl) return;
@@ -4837,6 +4848,10 @@ var RelayClient = class {
4837
4848
  // BL-126: echo the server-minted attribution key unchanged. Never invented here.
4838
4849
  ...dispatchId ? { dispatchId } : {},
4839
4850
  ...signature ? { signature } : {},
4851
+ // D13: additive marker for a lisa fresh-session (ephemeral one-shot) turn's
4852
+ // reply. Sent ONLY on lisa turns (true); every other flow reply omits the field
4853
+ // entirely, byte-identical to the pre-lisa body shape.
4854
+ ...lisaFresh ? { lisaFresh: true } : {},
4840
4855
  ...this.usageBody(usage)
4841
4856
  });
4842
4857
  const r = await this.httpJson(`/channels/${ch}/flows/${fl}/messages`, {
@@ -5092,6 +5107,16 @@ var SourceBroker = class {
5092
5107
  });
5093
5108
  return { socketPath: this.socketPath, token: this.token };
5094
5109
  }
5110
+ // G2 (lisa loop, D13 review Minor 3): the ephemeral one-shot lisa session (a blind
5111
+ // critic or builder round) inherits this SAME per-channel broker's MCP tools via the
5112
+ // startOpts replay in ChannelSession.runLisaFreshTurn (session.ts). That is safe
5113
+ // TODAY only because NO route below reads channel/flow MESSAGES (only sources,
5114
+ // reactions, notes, memory, tasks, artifacts, canvas). If a future route is added
5115
+ // here that reads channel or flow messages, it MUST be gated off lisa turns (or
5116
+ // otherwise structurally blocked from the ephemeral session) — reachable from a
5117
+ // blind critic, it would silently leak the transcript the relay's read-filter is
5118
+ // deliberately withholding (D9/G2b) and defeat critic blindness. Do not add a
5119
+ // channel/flow message-read route here without addressing this.
5095
5120
  async handle(req, res) {
5096
5121
  const send = (status, body2) => {
5097
5122
  const s = JSON.stringify(body2);
@@ -5913,6 +5938,16 @@ var AcpAdapter = class {
5913
5938
  this.noticeCbs.push(cb);
5914
5939
  },
5915
5940
  converseLocal: (text, overrideMode) => this.converseLocal(text, overrideMode),
5941
+ // Review Minor 3: converseLocalWithUsage is exposed ONLY on a NO-PERSISTENCE
5942
+ // (ephemeral, one-shot) construction — this.session undefined is the exact
5943
+ // invariant D13 already encodes (no persistence wired => AcpClient.start()'s
5944
+ // newSession path). A WARM adapter (session wired, e.g. createAdapter(cfg,
5945
+ // channelId)) never gets this method on its AgentSession at all, so a future
5946
+ // warm caller cannot silently advance the channel's disk-backed cumulative
5947
+ // cost baseline — the method simply does not exist to call.
5948
+ ...this.session ? {} : {
5949
+ converseLocalWithUsage: (text, overrideMode) => this.converseLocalWithUsage(text, overrideMode)
5950
+ },
5916
5951
  stop: async () => {
5917
5952
  this.stopped = true;
5918
5953
  await this.client?.stop();
@@ -5960,6 +5995,22 @@ var AcpAdapter = class {
5960
5995
  return `[braid] local turn failed: ${err?.message ?? String(err)}`;
5961
5996
  }
5962
5997
  }
5998
+ /**
5999
+ * D13/D20: like converseLocal, but for a lisa fresh-session (ephemeral one-shot)
6000
+ * turn — ALSO returns usage, converted via this.usageSource (for the ephemeral
6001
+ * construction path, an isolated non-persisted source; see createEphemeralLisaAdapter).
6002
+ * Deliberately does NOT swallow errors into an error-string reply: a lisa turn that
6003
+ * fails must fail loudly (nothing posts to the flow), never post a garbled
6004
+ * "local turn failed" string as though it were a real builder/critic report.
6005
+ */
6006
+ async converseLocalWithUsage(text, overrideMode) {
6007
+ const client = this.client;
6008
+ if (!client || this.stopped || this.gaveUp || this.handoffPaused) {
6009
+ throw new Error("[braid] agent unavailable for local conversation");
6010
+ }
6011
+ const result = await client.submit(text, overrideMode);
6012
+ return { text: result.text, usage: this.usageSource?.forTurn(result.usage) };
6013
+ }
5963
6014
  /**
5964
6015
  * Run one real channel turn against the warm session. On a turn that fails because
5965
6016
  * the subprocess crashed, supervise a restart (single-flight, capped) and best-effort
@@ -6262,13 +6313,23 @@ var ClaudeAdapter = class {
6262
6313
  };
6263
6314
  }
6264
6315
  };
6265
- function createAdapter(cfg, channelId, telemetry) {
6316
+ function acpPolicyAndClientFactory(cfg, telemetry) {
6266
6317
  const policy = {
6267
6318
  mode: cfg.toolMode,
6268
6319
  configDirAbs: resolve2(configDir(process.env)),
6269
6320
  agentName: cfg.agentName
6270
6321
  // for the once-per-process "braid tools full <name>" denial hint
6271
6322
  };
6323
+ const makeClient = (launch) => new AcpClient(
6324
+ launch,
6325
+ (l) => scopedLaunch(l, cfg, cfg.agent),
6326
+ afterChildExit,
6327
+ telemetry
6328
+ );
6329
+ return { policy, makeClient };
6330
+ }
6331
+ function createAdapter(cfg, channelId, telemetry) {
6332
+ const { policy, makeClient } = acpPolicyAndClientFactory(cfg, telemetry);
6272
6333
  if (cfg.agentMode !== "acp") return new ClaudeAdapter(policy, telemetry);
6273
6334
  if (channelId) {
6274
6335
  const prev = detectProviderSwitch(configDir(process.env), cfg.relayWsUrl, cfg.agentName, channelId, cfg.agent);
@@ -6288,14 +6349,13 @@ function createAdapter(cfg, channelId, telemetry) {
6288
6349
  }
6289
6350
  const session = channelId ? makeSessionPersistence(cfg, channelId) : void 0;
6290
6351
  const usageSource = channelId ? makeUsageSource(cfg, channelId) : void 0;
6291
- const makeClient = (launch) => new AcpClient(
6292
- launch,
6293
- (l) => scopedLaunch(l, cfg, cfg.agent),
6294
- afterChildExit,
6295
- telemetry
6296
- );
6297
6352
  return new AcpAdapter(cfg.agent, makeClient, void 0, policy, session, usageSource, cfg.agentName, cfg.stripEnvKeys, cfg.fallbackModel);
6298
6353
  }
6354
+ function createEphemeralLisaAdapter(cfg, telemetry) {
6355
+ const { policy, makeClient } = acpPolicyAndClientFactory(cfg, telemetry);
6356
+ const usageSource = makeEphemeralUsageSource(cfg);
6357
+ return new AcpAdapter(cfg.agent, makeClient, void 0, policy, void 0, usageSource, cfg.agentName, cfg.stripEnvKeys, cfg.fallbackModel);
6358
+ }
6299
6359
  function makeSessionPersistence(cfg, channelId) {
6300
6360
  const dir = configDir(process.env);
6301
6361
  return {
@@ -6326,6 +6386,22 @@ function makeUsageSource(cfg, channelId) {
6326
6386
  saveBaseline: (cumulative, currency) => updateStoredCost(dir, cfg.relayWsUrl, cfg.agentName, channelId, cumulative, currency, cfg.agent)
6327
6387
  });
6328
6388
  }
6389
+ function makeEphemeralUsageSource(cfg) {
6390
+ return new AcpUsageSource({
6391
+ model: cfg.model,
6392
+ provider: cfg.agent,
6393
+ authMode: effectiveUsageAuthMode({
6394
+ agent: cfg.agent,
6395
+ agentMode: cfg.agentMode,
6396
+ authMode: cfg.authMode ?? "auto",
6397
+ env: process.env,
6398
+ hasSubscriptionLogin: detectSubscriptionLogin(process.env)
6399
+ }),
6400
+ loadBaseline: () => void 0,
6401
+ saveBaseline: () => {
6402
+ }
6403
+ });
6404
+ }
6329
6405
 
6330
6406
  // src/providerPrompt.ts
6331
6407
  var PROVIDER_LABELS = {
@@ -7191,6 +7267,7 @@ export {
7191
7267
  interactiveHandoffSupported,
7192
7268
  interactiveResumeSpecFor,
7193
7269
  createAdapter,
7270
+ createEphemeralLisaAdapter,
7194
7271
  handoffCwdFor,
7195
7272
  PROVIDER_LABELS,
7196
7273
  chooseFirstRunProvider,
package/dist/cli.js CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  classifyFatal,
30
30
  configDir,
31
31
  createAdapter,
32
+ createEphemeralLisaAdapter,
32
33
  createWorkspaceNoticeGate,
33
34
  decodeInvite,
34
35
  distinctWorkspaceNames,
@@ -74,7 +75,7 @@ import {
74
75
  updateAgentWorkspace,
75
76
  validateRelayWsUrl,
76
77
  wsToHttp
77
- } from "./chunk-267GOUH7.js";
78
+ } from "./chunk-XUQU2KWB.js";
78
79
  import {
79
80
  clampUntrusted,
80
81
  externalSourceLabel,
@@ -289,7 +290,8 @@ function buildFlowPrompt(signal, agentName, channelId, toolMode = "readonly", ca
289
290
  ].filter((line, i, arr) => !(line === "" && arr[i - 1] === "")).join("\n");
290
291
  }
291
292
  const isSynthesis = signal.kind === "synthesis";
292
- const header = isSynthesis ? `You are ${agentName} and the TEAM LEAD for this Braid bounded discussion.` : `You are ${agentName} responding in an Braid bounded discussion.`;
293
+ const lisaRole = signal.lisa?.role;
294
+ const header = lisaRole === "builder" ? `You are the BUILDER in an iteration loop. Produce the next revision of the artifact. End with the COMMIT: <ref> line.` : lisaRole === "critic" ? `You are a BLIND CRITIC. Judge ONLY the artifact below \u2014 you cannot see the builder's reasoning or other critics. Your report MUST contain the ARTIFACT-SHA, SCORE, and WORST lines exactly as the instructions specify.` : isSynthesis ? `You are ${agentName} and the TEAM LEAD for this Braid bounded discussion.` : `You are ${agentName} responding in an Braid bounded discussion.`;
293
295
  const role = signal.rolePrompt ? `${fence("flow role description (supplied by flow configuration)", signal.rolePrompt)}
294
296
  ` : "";
295
297
  const ctx = signal.contextPrompt ? `${fence("flow context (supplied by flow configuration)", signal.contextPrompt)}
@@ -362,7 +364,7 @@ function externalTaintOf(messages) {
362
364
  return { preambleLabel: "an external source", fenceLabel: "external message" };
363
365
  }
364
366
  var ChannelSession = class {
365
- constructor(adapter, onReply, agentName, channelId, flow, fetchContext, beacon, toolMode = "readonly", holdCatchUpMaxMentions = 3, fetchPrivateContext, channelName, operator) {
367
+ constructor(adapter, onReply, agentName, channelId, flow, fetchContext, beacon, toolMode = "readonly", holdCatchUpMaxMentions = 3, fetchPrivateContext, channelName, operator, makeEphemeralAdapter) {
366
368
  this.adapter = adapter;
367
369
  this.onReply = onReply;
368
370
  this.agentName = agentName;
@@ -375,6 +377,7 @@ var ChannelSession = class {
375
377
  this.fetchPrivateContext = fetchPrivateContext;
376
378
  this.channelName = channelName;
377
379
  this.operator = operator;
380
+ this.makeEphemeralAdapter = makeEphemeralAdapter;
378
381
  }
379
382
  adapter;
380
383
  onReply;
@@ -388,6 +391,7 @@ var ChannelSession = class {
388
391
  fetchPrivateContext;
389
392
  channelName;
390
393
  operator;
394
+ makeEphemeralAdapter;
391
395
  session = null;
392
396
  roster = [];
393
397
  /** Replace the known peer roster (situational facts surfaced per message). */
@@ -434,6 +438,17 @@ var ChannelSession = class {
434
438
  * invocation regardless; a flow turn before any message frame narrows nothing.
435
439
  */
436
440
  lastBundleMode;
441
+ /** The options this session was started with (model, mcpServers, env, health, ...).
442
+ * Reused verbatim to construct each lisa ephemeral adapter's start() call, so a
443
+ * fresh-session turn runs with the SAME provider/tool wiring as the warm session
444
+ * minus persistence. Set once in start(); null before then.
445
+ * G2 (lisa loop, D13 review Minor 3): `mcpServers` here is this channel's broker
446
+ * tool advertisement, replayed VERBATIM into the ephemeral (possibly blind-critic)
447
+ * session below. Safe today only because the broker exposes no channel/flow
448
+ * message-READ route (see the matching note in src/mcp/broker.ts's handle()) — a
449
+ * future one would leak the transcript the relay deliberately withholds from a
450
+ * blind critic straight through this replay. */
451
+ startOpts = null;
437
452
  /** Mode-aware prompt head: the untrusted-content framing plus the one-line
438
453
  * truth about tool access (both OUTSIDE all fences, once per prompt).
439
454
  * `externalLabel`, when present (e.g. "Slack", "Discord"), selects the
@@ -456,6 +471,7 @@ ${toolStatusLine(mode, cause)}
456
471
  ${operatorFacts}`;
457
472
  }
458
473
  async start(opts) {
474
+ this.startOpts = opts;
459
475
  this.session = await this.adapter.start(opts);
460
476
  const notifier = makeTurnErrorNotifier((_ch, text) => this.onReply(text));
461
477
  this.session.onTurn((full, usage, turnId) => {
@@ -540,6 +556,80 @@ ${fence("reactions", formatReactions(msg.reactions))}
540
556
  this.pendingModes.push(effectiveMode);
541
557
  this.session.submit(capPrompt(head + instructions), narrowestOverride(effectiveMode, this.toolMode));
542
558
  }
559
+ /** BL-193: flow turns honor the channel's bound bundle mode for parity with the
560
+ * channel_message path — same narrow-only rule (G7: never widens), fed by the
561
+ * last-seen frame mode since flow signals carry none. The override threaded to
562
+ * converseLocal is the enforcement-layer half; effectiveMode + cause make the
563
+ * prompt framing truthful (BL-203: a full operator narrowed by the channel is
564
+ * told the CHANNEL restricts it, not its own mode). Shared by both the warm and
565
+ * the lisa-ephemeral flow-turn paths so they can never drift. */
566
+ flowModeAndCause() {
567
+ const effectiveMode = narrowestMode(this.lastBundleMode, this.toolMode);
568
+ const cause = effectiveMode === "readonly" && this.toolMode !== "readonly" ? "channel" : "operator";
569
+ return { effectiveMode, cause };
570
+ }
571
+ /**
572
+ * D13/D20 (lisa fresh-session turns): run the turn on a FRESH, ONE-SHOT provider
573
+ * session — never the channel's warm session/sessionStore entry, never
574
+ * saveStoredSession. Constructs the ephemeral adapter, starts it (no persistence
575
+ * wired ⇒ AcpClient's newSession path), submits EXACTLY ONE turn via
576
+ * converseLocalWithUsage, and tears it down in a finally so a failure mid-turn
577
+ * still disposes it. Posts the reply with lisaFresh:true and the turn's REAL usage
578
+ * (from the ephemeral adapter's own isolated usage source — see
579
+ * createEphemeralLisaAdapter). FAILURE POLICY (hard rule, D20): if the ephemeral
580
+ * adapter fails to start, OR the turn itself fails, this REJECTS — nothing posts,
581
+ * and the relay watchdog re-notifies the builder. Never falls back to the warm
582
+ * session or fabricates freshness/usage.
583
+ */
584
+ async runLisaFreshTurn(signal) {
585
+ if (!this.flow) {
586
+ throw new Error(
587
+ "[braid] lisa fresh-session turn requested but flow routing is unavailable for this session"
588
+ );
589
+ }
590
+ if (!this.makeEphemeralAdapter) {
591
+ throw new Error(
592
+ "[braid] lisa fresh-session turn requested but no ephemeral adapter factory is wired for this session"
593
+ );
594
+ }
595
+ const startOpts = this.startOpts;
596
+ if (!startOpts) throw new Error("ChannelSession not started");
597
+ const { effectiveMode, cause } = this.flowModeAndCause();
598
+ const prompt = buildFlowPrompt(
599
+ signal,
600
+ this.agentName,
601
+ this.channelId,
602
+ effectiveMode,
603
+ cause,
604
+ this.channelName?.(this.channelId)
605
+ );
606
+ this.beacon?.begin();
607
+ try {
608
+ const ephemeral = this.makeEphemeralAdapter();
609
+ const ephemeralSession = await ephemeral.start(startOpts);
610
+ try {
611
+ if (!ephemeralSession.converseLocalWithUsage) {
612
+ throw new Error("[braid] ephemeral lisa adapter does not support converseLocalWithUsage");
613
+ }
614
+ const { text, usage } = await ephemeralSession.converseLocalWithUsage(
615
+ capPrompt(prompt),
616
+ narrowestOverride(effectiveMode, this.toolMode)
617
+ );
618
+ await this.flow.postReply(signal.flowId, text.trim() || "(no response)", usage, signal.dispatchId, true);
619
+ } finally {
620
+ try {
621
+ await ephemeralSession.stop();
622
+ } catch (stopErr) {
623
+ console.warn(
624
+ "[braid] failed to dispose the ephemeral lisa adapter (leaked subprocess risk):",
625
+ String(stopErr?.message ?? stopErr)
626
+ );
627
+ }
628
+ }
629
+ } finally {
630
+ this.beacon?.end();
631
+ }
632
+ }
543
633
  /**
544
634
  * Run one bounded-flow turn or synthesis. Frames a MUST-RESPOND prompt (no
545
635
  * `<<silent>>`), runs it through the SAME warm session via converseLocal (so the
@@ -547,9 +637,18 @@ ${fence("reactions", formatReactions(msg.reactions))}
547
637
  * endpoint. A minimal turn_notification carries no transcript, so we backfill it
548
638
  * from the relay first. An empty reply still posts a fallback so the relay-blocked
549
639
  * flow advances rather than timing out.
640
+ *
641
+ * D13/D20: a signal carrying `lisa.freshSession:true` is routed to runLisaFreshTurn
642
+ * instead — an entirely separate, one-shot ephemeral session. Every other signal
643
+ * (including a lisa signal without freshSession) runs unchanged, below, on the warm
644
+ * session.
550
645
  */
551
646
  async runFlowTurn(signal) {
552
647
  if (!this.session) throw new Error("ChannelSession not started");
648
+ if (signal.lisa?.freshSession) {
649
+ await this.runLisaFreshTurn(signal);
650
+ return;
651
+ }
553
652
  if (!this.session.converseLocal || !this.flow) {
554
653
  console.warn("[braid] flow turn skipped: warm aside / flow routing unavailable");
555
654
  return;
@@ -557,9 +656,8 @@ ${fence("reactions", formatReactions(msg.reactions))}
557
656
  this.beacon?.begin();
558
657
  try {
559
658
  let history = signal.history;
560
- if (history.length === 0) history = await this.flow.fetchHistory(signal.flowId);
561
- const effectiveMode = narrowestMode(this.lastBundleMode, this.toolMode);
562
- const cause = effectiveMode === "readonly" && this.toolMode !== "readonly" ? "channel" : "operator";
659
+ if (history.length === 0 && !signal.lisa) history = await this.flow.fetchHistory(signal.flowId);
660
+ const { effectiveMode, cause } = this.flowModeAndCause();
563
661
  const prompt = buildFlowPrompt(
564
662
  { ...signal, history },
565
663
  this.agentName,
@@ -2537,7 +2635,8 @@ async function startBridge(cfg, relay, deps) {
2537
2635
  channelId,
2538
2636
  {
2539
2637
  // BL-126: thread dispatchId from the FlowSignal through to the POST body.
2540
- postReply: (flowId, content, usage, dispatchId) => relay.postFlowMessage(channelId, flowId, content, usage, dispatchId),
2638
+ // D13: also thread lisaFresh (true only for a lisa fresh-session reply).
2639
+ postReply: (flowId, content, usage, dispatchId, lisaFresh) => relay.postFlowMessage(channelId, flowId, content, usage, dispatchId, lisaFresh),
2541
2640
  fetchHistory: (flowId) => relay.fetchFlowMessages(channelId, flowId)
2542
2641
  },
2543
2642
  (queryHint) => relay.fetchChannelContext(channelId, queryHint),
@@ -2559,7 +2658,13 @@ async function startBridge(cfg, relay, deps) {
2559
2658
  // BL-272 Task 5 (D3/D6/D7/D8): the relay's live, per-hello platform-verified
2560
2659
  // operator attestation for THIS agent — a thunk (not a snapshot) so a
2561
2660
  // reconnect's fresh hello is reflected on the very next prompt.
2562
- () => relay.operator()
2661
+ () => relay.operator(),
2662
+ // D13/D20: construct a FRESH ephemeral adapter per lisa turn (never memoized).
2663
+ // Absent when the caller's deps omit makeEphemeralAdapter (e.g. a test using
2664
+ // only makeAdapter) — a lisa fresh-session signal then fails loudly instead of
2665
+ // silently running on the warm session. Ordered AFTER operator to match the
2666
+ // merged ChannelSession constructor param order (session.ts).
2667
+ deps.makeEphemeralAdapter ? () => deps.makeEphemeralAdapter(cfg) : void 0
2563
2668
  );
2564
2669
  const broker = new SourceBroker(channelId, relay);
2565
2670
  const brokerInfo = await broker.start();
@@ -2847,7 +2952,9 @@ async function createAndStartBridge(cfg, deps = {}) {
2847
2952
  );
2848
2953
  const rawMakeAdapter = deps.makeAdapter ?? createAdapter;
2849
2954
  const makeAdapter = (c, channelId) => rawMakeAdapter(c, channelId, deps.telemetry);
2850
- handle = await startBridge(cfg, relay, { makeAdapter, onReady: deps.onReady, cardCache: deps.cardCache, controlSocket: deps.controlSocket });
2955
+ const rawMakeEphemeralAdapter = deps.makeEphemeralAdapter ?? createEphemeralLisaAdapter;
2956
+ const makeEphemeralAdapter = (c) => rawMakeEphemeralAdapter(c, deps.telemetry);
2957
+ handle = await startBridge(cfg, relay, { makeAdapter, makeEphemeralAdapter, onReady: deps.onReady, cardCache: deps.cardCache, controlSocket: deps.controlSocket });
2851
2958
  return { relay, sessions: handle.sessions, channelNames: handle.channelNames, brokers: handle.brokers, hub: handle.hub, ensureSession: handle.ensureSession, controlSocketClose: handle.controlSocketClose };
2852
2959
  }
2853
2960
 
@@ -4,7 +4,7 @@ import {
4
4
  boundedJsonFetch,
5
5
  resetHttpMetricsForTest,
6
6
  snapshotHttpMetrics
7
- } from "./chunk-267GOUH7.js";
7
+ } from "./chunk-XUQU2KWB.js";
8
8
  import "./chunk-HQNAAXUM.js";
9
9
  export {
10
10
  assertHttpEndpoint,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowyroad/braid",
3
- "version": "0.94.0",
3
+ "version": "0.95.0",
4
4
  "description": "Braid: connect your own coding agent (Claude Code, Codex, Gemini, Grok) to a Braid channel and collaborate with other agents and humans.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "SnowyRoad",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@snowyroad/braid",
3
- "version": "0.94.0",
3
+ "version": "0.95.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@snowyroad/braid",
9
- "version": "0.94.0",
9
+ "version": "0.95.0",
10
10
  "license": "SEE LICENSE IN LICENSE.md",
11
11
  "dependencies": {
12
12
  "@agentclientprotocol/sdk": "0.25.1",
@@ -2369,9 +2369,9 @@
2369
2369
  }
2370
2370
  },
2371
2371
  "node_modules/hono": {
2372
- "version": "4.13.4",
2373
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.4.tgz",
2374
- "integrity": "sha512-AGEwKIyRMHRv1t8Wjwa3LHxQ61X5CqrdFT+4BRNTpqS5aJNnpl5WLjADb7vFlJzI/8uK7T5QLVApCMQKNa3LgQ==",
2372
+ "version": "4.13.5",
2373
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.5.tgz",
2374
+ "integrity": "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw==",
2375
2375
  "license": "MIT",
2376
2376
  "engines": {
2377
2377
  "node": ">=16.9.0"